apple-photos-mcp
This server acts as a read-only bridge enabling AI assistants to query, browse, and export photos from a macOS Apple Photos library. All data stays local — nothing is sent to external servers.
Discovery & Diagnostics
Health Check – Verify that
osxphotosis installed and the Photos library is accessibleLibrary Info – Get high-level stats: total counts of photos, movies, albums, folders, keywords, and persons
Querying & Search
Query – Search photos using any combination of filters: UUIDs, album, keyword, person/face, date range (ISO 8601), favorite/hidden status, media type, title/description substring, and result limit
Get Photo – Retrieve full metadata for a single photo by UUID, including dimensions, dates, GPS location, place name, albums, keywords, persons, labels, file paths, and type flags (HDR, live, raw, edited, portrait, panorama, selfie, screenshot, slow-mo, time-lapse, burst)
Browsing Library Structure
List Albums – All albums with folder paths, photo counts, shared status, and UUIDs
List Folders – All folders with parent folder, album count, and subfolder count
List Keywords – All keywords sorted by usage count (with optional top-N limit)
List Persons – All people detected by face recognition, sorted by photo count (with optional top-N limit)
Export
Export Photos – Export one or more photos by UUID to a destination directory, with options to export the original or edited version, include live-photo video, include raw image sidecar (NEF, CR2, etc.), overwrite existing files, and auto-download iCloud-only originals via Photos.app
Additional Notes
All operations are read-only against the Photos library (exports write to a directory you specify)
Supports targeting a non-default
.photoslibraryvia thelibraryparameter on any tool
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@apple-photos-mcpFind my photos from Paris last summer"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Apple Photos MCP Server
A Model Context Protocol (MCP) server that enables AI assistants like Claude to query, search, export, and inspect the macOS Apple Photos library — plus opt-in album and metadata write tools — backed by the osxphotos library.
Read-only by default. Out of the box the library is never modified — exports write files to a directory you choose, nothing more. A set of opt-in write tools (albums, titles, descriptions, keywords, favorites, dates, imports — never deletion) unlocks only when you explicitly set
APPLE_PHOTOS_MCP_ENABLE_WRITES=1; without that flag, 2.x behaves exactly like the read-only 1.x releases.
What is This?
This server acts as a bridge between AI assistants and Apple Photos. Once configured, you can ask Claude (or any MCP-compatible AI) to:
"Find all my photos from our trip to Spain in 2023"
"Show me my favorite sunset photos" — and actually see them (
get-thumbnailreturns viewable images)"How many photos do I have? What are my top keywords?"
"Find photos of Sarah from last summer and export them to ~/Desktop/sarah-summer"
"What did I import this week?" (
addedInLast), "Find my screenshots from 2024""Do I have duplicate photos?" (
find-duplicatesgroups exact duplicates)"List my albums"
"Tell me everything about photo UUID ABC-123" — including EXIF camera data
With writes enabled: "File these into a Trailcam album", "Tag them all
deer", "Favorite the best one and caption it"
The AI assistant communicates with this server, which uses osxphotos to read the Photos library SQLite database directly. All data stays local on your machine.
Related MCP server: iOS MCP Server
Quick Start
Using Claude Code (Easiest)
If you're using Claude Code (in Terminal or VS Code), just ask Claude to install it:
Install the sweetrb/apple-photos-mcp MCP server so you can help me query my Apple Photos libraryClaude will handle the installation and configuration automatically. Or register it yourself with one deterministic command:
claude mcp add apple-photos -s user -- npx -y apple-photos-mcpThe Python osxphotos dependency installs automatically on first use (a one-time, ~minute-long setup), so the only manual step is granting Full Disk Access — see Requirements below.
Using the Plugin Marketplace
Install as a Claude Code plugin for automatic configuration and enhanced AI behavior:
/plugin marketplace add sweetrb/apple-photos-mcp
/plugin install apple-photosThis method also installs a skill that teaches Claude when and how to use Apple Photos effectively.
A few things to know about the plugin install:
The plugin is a git clone under
~/.claude/plugins/marketplaces/apple-photos-mcp/, and the server runs straight from that clone (no build step needed).The first tool call auto-bootstraps a Python venv with
osxphotosinside that clone — a one-time, ~minute-long setup that requires Python 3.11+ on your PATH (stock macOS ships 3.9;brew install python@3.12).Full Disk Access must be granted to the HOST app running Claude Code (Terminal, iTerm, VS Code, Claude Desktop) — see Requirements below.
Using the Codex Marketplace
The same plugin is available for Codex. Add the marketplace and install the plugin:
codex plugin marketplace add sweetrb/apple-photos-mcp
codex plugin add apple-photos@apple-photos-mcpThe Codex plugin runs the published apple-photos-mcp server through npx and ships the same Apple Photos skill, so behavior matches the Claude Code plugin. Because the server is a Python-sidecar (osxphotos) server, the first tool call after an npx launch auto-bootstraps a project-local Python venv with osxphotos (a one-time, ~minute-long setup), and the host process still needs Full Disk Access — see Requirements below.
Other Hosts (Hermes, Antigravity)
Two more hosts can run the same apple-photos MCP server (npx -y apple-photos-mcp). As a Python-sidecar (osxphotos) server it also needs Full Disk Access; see Requirements.
Hermes Agent (NousResearch) — Hermes has no plugin/marketplace drop-in, so there is nothing in this repo to install from. Register the server with the CLI:
hermes mcp add apple-photos --command npx --args -y apple-photos-mcpOr add it to
~/.hermes/config.yamlby hand:mcp_servers: apple-photos: command: npx args: ["-y", "apple-photos-mcp"]Restart your Hermes session afterward so the tools load.
Antigravity (Google) — add the server entry from
.antigravity-plugin/mcp_config.jsonto~/.gemini/config/mcp_config.json(or via Antigravity's MCP settings).
Manual Installation
1. Install the server:
npm install -g apple-photos-mcp2. Python deps install automatically. The first tool call auto-bootstraps a project-local Python venv with osxphotos (a one-time setup that can take ~a minute; progress is logged to stderr). You do not need to install anything by hand.
To skip the first-call delay, you can pre-warm the venv ahead of time:
pnpm run setup # optional — pre-installs osxphotos so the first tool call is instantAuto-setup needs Python 3, pip, and network access. If any are missing — or you disabled auto-setup via APPLE_PHOTOS_MCP_NO_AUTO_SETUP=1 — run pnpm run setup (or pip3 install osxphotos) yourself. See Configuration and Troubleshooting.
3. Add to Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"apple-photos": {
"command": "npx",
"args": ["apple-photos-mcp"]
}
}
}4. Grant Full Disk Access to the app hosting the MCP server (Claude Desktop, Terminal, VS Code, etc.) — see Full Disk Access below.
5. Restart Claude Desktop and start using natural language:
"How many photos are in my library?"Requirements
macOS - The Photos library is macOS-only
Node.js 20+ - Required for the MCP server
Python 3.11+ - The server uses osxphotos under the hood and installs it automatically on first use into a project-local venv (one-time, ~a minute). You only need Python 3.11+,
pip, and a network connection available. osxphotos requires Python ≥ 3.10 and the date filters need 3.11; macOS ships 3.9, so install a newer Python first (e.g.brew install python@3.12). Pre-warm it withpnpm run setupif you'd rather not wait on the first call.Apple Photos - Must have a Photos library (default location:
~/Pictures/Photos Library.photoslibrary)Full Disk Access - The Photos library lives in a protected directory. The host app needs Full Disk Access — see below and the Full Disk Access Setup Guide.
Features
Querying
Feature | Description |
Library Stats | Total counts of photos, movies, albums, folders, keywords, persons |
Query | Search by taken-date or import-date range ( |
Photo Details | Full metadata for one photo: dimensions, location, place, EXIF camera data (make/model, lens, ISO, aperture, shutter speed, focal length), Photos' ML intelligence (aesthetic |
Batch Details |
|
Selection Bridge |
|
Thumbnails |
|
Find Duplicates |
|
List Albums | All albums with their folder paths and photo counts |
List Folders | All folders with parent and album/subfolder counts |
List Keywords | Keywords sorted by usage count |
List Persons | People detected by Photos face recognition, sorted by photo count |
Export
Feature | Description |
Export Originals | Copy original photos to a destination directory |
Export Edited | Copy the edited version instead of the original |
Live Photos | Optionally include the live-photo video alongside the still |
Raw Files | Optionally include the raw (NEF, CR2, etc.) sidecar |
Multi-photo Export | Export multiple UUIDs in a single call |
Auto iCloud Download | If an original isn't on disk, export falls back to Photos.app to download it on demand — no extra parameter needed |
Write tools (opt-in — read-only by default)
Feature | Description |
Create Album |
|
Add to Album |
|
Remove from Album |
|
Set Metadata |
|
Set Keywords |
|
Set Date |
|
Import |
|
All seven are disabled unless APPLE_PHOTOS_MCP_ENABLE_WRITES=1 — see Write tools (opt-in). None of them can delete a photo.
Diagnostics
Feature | Description |
Health Check | Verify osxphotos is installed and the library can be opened |
Doctor | Richer setup diagnostic — six checks: Python interpreter (path + version), osxphotos install, sidecar mode (persistent vs one-shot fallback), the write-tools gate (enabled/disabled + backend readiness), Photos library readability, and Full Disk Access, each reported ok / warn / fail with actionable advice |
Read tools also return structured JSON (structuredContent) alongside the human-readable text, so agents can consume results without parsing prose.
MCP resources & prompts
Resources expose read-only context the client can attach without a tool call:
photos://library, photos://albums, photos://persons, photos://keywords,
and the photos://photo/{uuid} template (full metadata for one photo). Prompts
package common workflows: find-photos, export-photos, photo-summary.
Write tools (opt-in)
The server is read-only by default — nothing changes for existing users. Version 2.0.0 added five tools that can modify the Photos library (create-album, add-to-album, remove-from-album, set-photo-metadata, set-keywords), and 2.1.0 adds two more (set-photo-date, import-photos) behind the same gate. Every one of them is refused with a clear error until you opt in:
Enable via environment variable (e.g. in your MCP server config's env block, where the host honors it):
{ "env": { "APPLE_PHOTOS_MCP_ENABLE_WRITES": "1" } }Or via the config file (recommended for Claude Desktop, which strips env) — ~/Library/Application Support/apple-photos-mcp/config.json:
{
"APPLE_PHOTOS_MCP_ENABLE_WRITES": "1"
}Then restart the MCP server (restart the host app, or the conversation in hosts that spawn per-conversation servers). The doctor tool reports the gate state either way — run it first if a write tool returns "Write tools are disabled".
The write tools stay registered even while disabled (MCP clients cache the tool list at startup, so hiding them would only hurt discoverability — a gated call returns the exact opt-in recipe instead).
Safety design
No deletion, ever. There is deliberately no tool that deletes a photo, an album, or a folder.
remove-from-albumchanges album membership only — the photos stay in All Photos and every other album. For actual deletion, quarantine photos into an album (the dedupe pattern) and delete inside Photos.app, where Recently Deleted gives you a 30-day safety net. The same no-delete rule cuts the other way forimport-photos: an import cannot be programmatically undone (Photos' AppleScript has no photo-delete verb), so removing a mistaken import is a by-hand operation in Photos.app.Explicit targets only. Every write takes explicit UUIDs, names, or file paths — there are no wildcard/all-photos operations, and every target is validated to exist before anything is modified (unknown UUIDs come back as clear errors or per-UUID
notFoundlists; import source paths must exist under the same allowlist roots as export).Bounded batches. Album operations accept at most 100 UUIDs per call; imports at most 50 files.
Reversible by design. Metadata writes echo before/after values so an agent can revert;
set-keywordsuses union semantics (read-merge-write) so keywords you don't mention are never clobbered; album adds are idempotent;set-photo-dateis a dry run by default — it writes nothing until you passdryRun: false, and always echoes before/after so an applied change can be reverted.The mechanism: writes drive Photos.app via AppleScript (the photoscript library). Photos is launched if it isn't running, and macOS asks for Automation permission for the host app with a one-time system prompt on the first write. Writes always target the library currently open in Photos.app (normally the system library) — the
libraryparameter of the read tools does not apply. (Reads, by contrast, go through osxphotos straight to the Photos database — fast and prompt-free. Why writes can't use that same path — and why osxphotos/PhotoKit aren't an escape from AppleScript — is spelled out in docs/WRITE-BACKEND.md.)One quirk to know: Photos' AppleScript dictionary has no "remove from album" verb, so
remove-from-albumrebuilds the album (same name, remaining photos): the album's UUID changes (the response reports old and new) and any custom manual sort order is lost.
Tool Reference
This section documents all available tools. AI agents should use these tool names and parameters exactly as specified.
Discovery
health-check
Verify osxphotos is installed and the Photos library can be opened.
Parameters: None
Returns: osxphotos version, library path, and total photo count — or an error if the library is inaccessible.
doctor
Run a full setup diagnostic — six checks: the resolved Python interpreter (path + version — warns when it's older than the required 3.11, with brew install python@3.12 advice), osxphotos installation, sidecar mode (persistent vs one-shot fallback, plus the last respawn), the write-tools gate (enabled/disabled, with the opt-in recipe and — when enabled — whether the photoscript backend and Photos.app look usable), Photos library readability, and Full Disk Access — each reported as ok / warn / fail with an actionable message. This is the richer counterpart to health-check; reach for it first when a tool returns a permission or "unable to open" error.
Parameters: None
Returns: A per-check report. The structuredContent carries the raw { healthy, checks[] }, where each check has name, status (ok/warn/fail), and detail. The Full Disk Access check explicitly reports whether the host process can read the library — see Full Disk Access.
library-info
High-level stats about the Photos library.
Parameter | Type | Required | Description |
| string | No | Path to a non-default |
Returns: Library path, Photos DB version, Photos.app version, counts of photos / movies / albums / folders / keywords / persons.
Query
query
Search the library with combinable filters. Returns photo summaries with UUIDs — use get-photo for full details on a specific match.
For the full filter syntax — accepted date forms, AND/OR combination semantics, exact-vs-substring matching, result ordering, and what is not filterable — see the Query Guide.
Parameter | Type | Required | Description |
| string[] | No | Specific UUIDs to fetch (max 1000 entries, each ≤ 256 chars) |
| string[] | No | Album name(s); ANY-match, exact full folder path + name (max 100 entries) |
| string[] | No | Keyword(s); ANY-match, exact whole-string (max 100 entries) |
| string[] | No | Person name(s); ANY-match, exact whole-string (max 100 entries) |
| string | No | ISO 8601 inclusive lower bound on photo date (e.g. |
| string | No | ISO 8601 upper bound on photo date. A bare date (e.g. |
| boolean | No | Only favorites |
| boolean | No | Exclude favorites |
| boolean | No | Only hidden photos |
| boolean | No | Exclude hidden photos (default behavior) |
| boolean | No | Include still photos |
| boolean | No | Include movies |
| string | No | Substring match on title (case-sensitive, ≤ 1024 chars) |
| string | No | Substring match on description (case-sensitive, ≤ 2048 chars) |
| string | No | ISO 8601 inclusive lower bound on IMPORT date ( |
| string | No | ISO 8601 upper bound on import date; a bare date includes that whole day |
| string | No | Imported within a trailing window — |
| string[] | No | ML classification label(s) Photos computed (the |
| string[] | No | Folder name(s)/path(s) — photos in albums inside the folder; ANY-match (max 100) |
| string[] | No | Place-name substring(s) from reverse geocoding (city, region, landmark). Multiple values are ANDed, not ORed (max 100) |
| boolean | No |
|
| string | No | GPS-radius filter: |
| number[] | No | Taken in calendar year(s); ANY-match (max 100) |
| number | No | Original file size at least this many bytes |
| number | No | Original file size at most this many bytes |
| boolean | No | Only photos carrying no keyword at all |
| boolean | No | Only burst photos |
| boolean | No | Media-type filters — each |
| boolean | No | Only videos/movies (alias of |
| number | No | Only photos whose Photos-computed overall aesthetic score (0–1) is at least this (e.g. |
| string | No | Case-insensitive substring over the text Photos' own OCR indexed per photo (macOS 13+) — receipts, signs, screenshots. Post-filter that reads per-photo search info, so combine with narrowing filters on big libraries |
| boolean | No | Sort by taken date, newest first, before |
| number | No | Cap the number of results returned (default |
| string | No | Path to a non-default |
Exceeding a cap rejects the call at the input schema, before the library is opened — chunk larger UUID batches across multiple calls.
Example - Recent favorites of Sarah:
{
"person": ["Sarah"],
"favorite": true,
"fromDate": "2025-06-01",
"limit": 50
}Example - Sunset keyword across two albums:
{
"keyword": ["sunset"],
"album": ["Vacation 2024", "Beach Trips"]
}Example - The 20 most recent imports:
{
"addedInLast": "7d",
"newestFirst": true,
"limit": 20
}Example - 2024 screenshot cleanup candidates:
{
"screenshot": true,
"year": [2024]
}Example - The best shots taken near the cabin:
{
"near": "46.51,-87.42,5",
"minScore": 0.6,
"newestFirst": true,
"limit": 20
}Returns: count (the total number of matches), returned (the number of summaries in this response — capped at limit, default 500), and photo summaries (UUID, filename, date, dimensions, favorite/hidden flags, albums, keywords, persons).
get-photo
Get full metadata for a single photo by UUID.
Parameter | Type | Required | Description |
| string | Yes | Photo UUID, as returned by |
| boolean | No |
|
| string | No | Path to a non-default |
Example:
{
"uuid": "33AC0410-D367-43AE-A839-12C7EF482020"
}Returns: All metadata for the photo: dimensions, original dimensions, dates (taken/added/modified), title, description, location (lat/lon), place (name/country), albums, keywords, persons, labels, an exif object (camera make/model, lens, ISO, aperture, shutter speed, focal length, exposure bias, flash, and duration/fps/codec for video — null when Photos recorded no EXIF, e.g. manufacturer-app uploads and scans), Photos' ML intelligence (score — the overall aesthetic score 0–1; detectedText — the text Photos' OCR indexed, macOS 13+; both null on library versions without them), iCloud shared-album social data (owner, comments, likes — only populated for shared assets), type flags (HDR / live / raw / edited / portrait / panorama / selfie / screenshot / slow-mo / time-lapse / burst), file paths (original, edited, raw, live-photo video), file size, UTI.
Recently Deleted: get-photo falls back to the trash, so it returns full metadata even for a photo sitting in Recently Deleted. query and export read the main library only — so a UUID that get-photo resolves may return nothing from query, and export will skip it with reason UUID not found (deleted or in trash).
get-photos
Get full metadata for a batch of photos (up to 50) in one call — the batch equivalent of get-photo, for dedupe reviews, EXIF audits, and captioning passes.
Parameter | Type | Required | Description |
| string[] | Yes | 1–50 photo UUIDs, as returned by |
| string | No | Path to a non-default |
Example:
{
"uuid": [
"33AC0410-D367-43AE-A839-12C7EF482020",
"1EB2B765-0765-43BA-A90C-0F0AE547B343"
]
}Returns: count, photos (full per-photo detail — the same shape as get-photo, including the exif object, score, detectedText, shared-album owner/comments/likes, and the Recently-Deleted fallback), and notFound listing any requested UUIDs that matched nothing. Unknown UUIDs never fail the batch.
get-selected-photos
Get the photos currently selected in the Photos.app window — the bridge from "act on these photos" to UUIDs you can feed into get-photos, get-thumbnail, export, or add-to-album.
No parameters.
Requirements & behavior:
Photos.app must be running with a visible selection; the tool returns a clear error otherwise, and never launches Photos itself.
Read-only and not gated behind the writes flag, but it reads the selection over AppleScript, so the host app needs macOS Automation permission for Photos (one-time system prompt on first use).
The selection comes from the library currently open in Photos.app; there is no
libraryparameter.
Returns: count, photos (the same summary shape as query results — UUID, filename, date, dimensions, flags), and notFound for selected items the library index doesn't know yet (e.g. a just-finished import Photos hasn't checkpointed — reported with their filenames).
get-thumbnail
Return one photo as an inline viewable image — an MCP image content block (base64 JPEG/PNG) that vision-capable clients render directly. Serves the preview derivatives Photos has already generated, so nothing is exported and originals aren't transferred. Prefer this over export whenever the goal is to look at a photo rather than to obtain the file.
Parameter | Type | Required | Description |
| string | Yes | Photo UUID, as returned by |
| number | No | Smallest acceptable long-edge size in pixels (default |
| string | No | Path to a non-default |
Returns: An image content block plus structured metadata: uuid, source path, width/height, mimeType, byteSize, and isDerivative (false means no suitable derivative existed and the image was rendered from the original via sips — never upscaled). Movies get a thumbnail only when Photos generated a poster-frame derivative; an iCloud-only photo with no local derivative or original returns an error suggesting export (which downloads on demand). Responses are capped at 8 MB: derivative selection pre-filters on that cap, but a sips-rendered fallback from a very high-resolution original can still exceed it and returns thumbnail is <n> bytes (cap 8388608); request a smaller minSize — lower minSize, or use export for the full file.
find-duplicates
Group exact duplicates using Photos' own fingerprint-based detection — the same data behind Photos' Duplicates album, no export or hashing required.
Parameter | Type | Required | Description |
| number | No | Max duplicate groups to return (default |
| string | No | Path to a non-default |
Returns: groupCount (total groups), returned, and groups ordered newest-first — each with the member uuids and per-member filename, date, size, width/height, and isMovie. Hidden and Recently-Deleted photos are never group members.
Exact means exact: the fingerprint matches identical image data only — edited copies, resized versions, and burst siblings will NOT group. Use get-thumbnail on a group's members to eyeball them before acting. This server cannot delete photos (Photos exposes no scriptable delete) — to act on duplicates, quarantine the extra copies into an album (create-album + add-to-album when writes are enabled, otherwise by hand in Photos.app) and review/delete inside Photos.app. See the dedupe pattern.
Browse
list-albums
List all albums in the library.
Parameter | Type | Required | Description |
| string | No | Path to a non-default |
Returns: Each album's title, folder path, photo count, shared status, and UUID. iCloud Shared Albums are included and flagged isShared: true.
list-folders
List all folders in the library.
Parameter | Type | Required | Description |
| string | No | Path to a non-default |
Returns: Each folder's title, parent folder, album count, and subfolder count.
list-keywords
List keywords sorted by usage count.
Parameter | Type | Required | Description |
| number | No | Cap to top-N keywords (max |
| string | No | Path to a non-default |
Returns: Keywords with their photo counts, sorted descending.
list-persons
List people detected by Photos face recognition, sorted by photo count.
Parameter | Type | Required | Description |
| number | No | Cap to top-N persons (max |
| string | No | Path to a non-default |
Returns: Persons with their photo counts, sorted descending. Unidentified faces appear as _UNKNOWN_.
Export
export
Export one or more photos by UUID to a destination directory.
Parameter | Type | Required | Description |
| string[] | Yes | Photo UUID(s) to export (1–1000 entries, each ≤ 256 chars) |
| string | Yes | Destination directory (created if missing). Must resolve — after expanding |
| boolean | No | Export the edited version instead of the original |
| boolean | No | Also export the live-photo video |
| boolean | No | Also export the raw image |
| boolean | No | Overwrite existing files at the destination. Without it, a photo whose file already exists is skipped (reported per-UUID) — never duplicated |
| string | No | Path to a non-default |
Example - Originals to a folder:
{
"uuid": ["33AC0410-...", "EEFCEF1D-..."],
"dest": "~/Desktop/exports"
}Example - Edited versions plus raw and live-photo video:
{
"uuid": ["33AC0410-..."],
"dest": "~/Desktop/exports",
"edited": true,
"raw": true,
"live": true,
"overwrite": true
}Returns: Destination path, count of files exported, count skipped, list of exported file paths, and a per-UUID reason for every skip (file already exists, UUID not found / in Recently Deleted, iCloud download failed, ...). Every requested UUID is accounted for in exported + skipped. Note that export reads the main library only: a photo in Recently Deleted is skipped with UUID not found (deleted or in trash) even though get-photo still resolves it (that tool falls back to the trash).
Destination allowlist: the destination is canonicalized (leading ~ expanded, .. normalized, symlinks resolved — including a not-yet-existing final directory) and must land under the home directory, /tmp, /private/tmp, or /Volumes. The check is segment-aware (/Volumesx does not pass as /Volumes), and the canonical path is what's exported into, so a symlink under an allowed root can't redirect the write outside it.
Filename collisions: Files keep the photo's original filename. If a file of that name already exists at the destination and overwrite is not set, the photo is skipped with reason already exists at destination — re-running an export never creates IMG_1234 (1).jpg-style duplicates. Pass overwrite: true to replace in place.
iCloud-only originals: If a photo's original isn't on disk (Photos is using "Optimize Mac Storage"), the export automatically falls back to Photos.app via AppleScript, which downloads the original on demand — same behavior as opening the photo in Photos. This is slower than a direct file copy; expect waits proportional to download size for large batches. Photos that genuinely can't be exported (e.g. edited=true requested but no edits exist) are still skipped with a per-UUID reason.
Progress notifications: For batch exports, the server emits one MCP progress notification per photo (progress/total plus a message naming the file being exported) when the client's request includes a progressToken — so hosts that surface progress can show a live counter instead of a silent multi-minute call. Clients that don't send a token simply get the final result, as before. (Progress requires the persistent sidecar; in the rare one-shot fallback mode the export still works but reports no intermediate progress.)
Write (opt-in — see Write tools)
All seven tools below require APPLE_PHOTOS_MCP_ENABLE_WRITES=1 and return a clear opt-in error otherwise. They drive Photos.app via AppleScript (macOS Automation permission; Photos is launched if needed), always target the library currently open in Photos.app (no library parameter), and can never delete photos.
create-album
Create an album — or return the existing one of that name (created: false), so re-running a filing workflow never piles up duplicates.
Parameter | Type | Required | Description |
| string | Yes | Album name (≤ 255 chars) |
| string | No | Folder path to nest the album under, |
Returns: album {uuid, name, path} and created. Without folder, the idempotency check matches an album of that name anywhere in the library; with folder, only inside that folder.
add-to-album
Add photos (by UUID) to an album (by name or UUID). Idempotent — Photos albums are sets.
Parameter | Type | Required | Description |
| string | Yes | Album name or UUID (UUID-looking values try the id lookup first, then fall back to a name match) |
| string[] | Yes | Photo UUID(s) to add (1–100) |
Returns: album {uuid, name, path}, addedCount, added, alreadyPresent (members already in the album), and notFound (UUIDs that don't exist in the library). Fails only when the album doesn't exist or no requested photo exists.
remove-from-album
Remove photos from an album — never from the library (they remain in All Photos and every other album).
Parameter | Type | Required | Description |
| string | Yes | Album name or UUID |
| string[] | Yes | Photo UUID(s) to remove from the album (1–100) |
Returns: the album after the operation, removedCount, removed, notInAlbum (requested UUIDs that weren't members — harmless no-ops), albumRecreated, and previousAlbumUuid.
Album rebuild caveat: Photos' AppleScript has no remove verb, so removal rebuilds the album (create replacement → copy the kept photos → delete the original → rename). The album's UUID changes (use album.uuid from the response) and custom manual sort order is lost. When none of the UUIDs are members, nothing is rebuilt (albumRecreated: false).
If a rebuild is interrupted: the replacement is built under a scratch name apple-photos-mcp-tmp-<hex> and renamed last. If the call is killed mid-rebuild — e.g. the 10-minute rebuild budget expires while copying a very large album — the original album and every photo are safe, but an apple-photos-mcp-tmp-… album may be left behind; delete it in Photos.app. In the rare case the kill lands in the brief window after the original was deleted, the kept photos are still safe under that scratch name — rename it back. The scratch name is picked fresh (and collision-checked) per call, so repeatedly retrying a timing-out removal strands a distinct album each time.
set-photo-metadata
Set a photo's title, description, and/or favorite flag. Only the fields you pass are touched.
Parameter | Type | Required | Description |
| string | Yes | Photo UUID |
| string | No | New title (≤ 255 chars; empty string clears it) |
| string | No | New description (≤ 2048 chars; empty string clears it) |
| boolean | No | Set or clear the favorite flag |
Returns: uuid, updated (which fields were written), and full before / after values of all three fields — revert a change by writing the before values back.
set-keywords
Add and/or remove keywords on a photo with union semantics: the photo's current keywords are read first and the edits merged in, so keywords you don't mention are always preserved — never a blind replace.
Parameter | Type | Required | Description |
| string | Yes | Photo UUID |
| string[] | No | Keywords to add (≤ 100 entries, each ≤ 255 chars; created in Photos if new) |
| string[] | No | Keywords to remove from this photo (≤ 100 entries, each ≤ 255 chars; exact match) |
At least one of add / remove is required; a keyword in both is rejected.
Returns: uuid, before / after keyword lists, added / removed (what actually changed — adding an existing keyword is a no-op), and changed. If the merge changes nothing, no write is performed.
set-photo-date
Fix a photo's date/time — set an absolute date or shift by a number of seconds. Dry run by default: nothing is written until you pass dryRun: false. This rewrites the date in the Photos library database only (the same thing Photos.app's Adjust Date & Time does) — the file's EXIF is never modified.
Parameter | Type | Required | Description |
| string | Yes | Photo UUID |
| string | * | Absolute new date-time, ISO 8601 (e.g. |
| number | * | Shift the current date by this many seconds (negative = earlier; |
| boolean | No | Default |
* Exactly one of date / shiftSeconds is required.
Returns: uuid, before, after (the would-be date on a dry run), shiftSeconds (the effective delta), applied, and dryRun. Revert an applied change by re-running with date = the echoed before and dryRun: false.
import-photos
Import image/video files from disk into the Photos library, optionally straight into an existing album. Add-only: nothing is modified or deleted, and source files stay where they are (Photos copies them in).
Parameter | Type | Required | Description |
| string[] | Yes | 1–50 absolute (or |
| string | No | Existing album (name or UUID) to file the imports into — create it with |
| boolean | No | Default |
Returns: requestedCount, importedCount, imported (uuid + filename per new item), and album when one was targeted. importedCount < requestedCount usually means Photos skipped duplicates.
Cannot be undone programmatically: Photos' AppleScript has no photo-delete verb, so removing a mistaken import means deleting it by hand in Photos.app.
Usage Patterns
Getting query filters right (date forms, AND/OR semantics, exact-match rules, ordering, paging) is covered in the Query Guide.
Basic Workflow
User: "How many photos do I have?"
AI: [calls library-info]
"You have 30,968 items: 30,435 photos and 533 movies across 46 albums..."
User: "Find my favorite sunset photos"
AI: [calls query with keyword=["sunset"], favorite=true]
"Found 12 favorite sunset photos. Here are the most recent..."
User: "Tell me about the first one"
AI: [calls get-photo with uuid="..."]
"Taken on 2025-09-14 at 19:47, in Big Sur..."Two-step: Query then Export
User: "Export all photos of Mollee from the beach to ~/Desktop/mollee-beach"
AI: [calls query with person=["Mollee"], keyword=["beach"]]
"Found 109 photos."
AI: [calls export with the UUIDs and dest="~/Desktop/mollee-beach"]
"Exported 109 files to ~/Desktop/mollee-beach."Seeing Photos: Query then Thumbnail
User: "Show me the best photo from Saturday"
AI: [calls query with fromDate/toDate for Saturday, newestFirst=true]
"Found 14 photos from Saturday."
AI: [calls get-thumbnail on a few candidates — the images render inline]
"This one of the lake at sunset is the standout..."Reviewing Recent Imports
User: "What came off the camera this week?"
AI: [calls query with addedInLast="7d", newestFirst=true, limit=20]
"23 items imported in the last 7 days; here are the 20 newest..."
User: "Which of those have no keyword yet?"
AI: [calls query with addedInLast="7d", noKeyword=true]
"9 of them are untagged."Duplicate Cleanup
User: "Do I have duplicate photos?"
AI: [calls find-duplicates]
"312 groups of exact duplicates."
AI: [calls get-thumbnail on members of the first few groups to verify visually]
"Each group is byte-identical — e.g. IMG_3588.HEIC appears twice..."
AI: "I can't delete photos (read-only) — collect one copy of each into a
quarantine album in Photos.app and delete from there."With writes enabled, the AI can build that quarantine album itself — the album-quarantine pattern (deletion still happens only in Photos.app, with its 30-day Recently Deleted safety net):
User: "Quarantine the duplicate extras for me."
AI: [calls create-album name="Duplicates — review & delete"]
AI: [calls add-to-album with every group's extra copies (keeping the best of each)]
"312 extra copies are in 'Duplicates — review & delete'.
Review the album in Photos.app and delete from there."Tagging and Filing (write tools)
User: "Tag this week's trailcam imports and file them into the Trailcam album"
AI: [calls query addedInLast="7d"] → UUIDs
AI: [calls create-album name="Trailcam"] (idempotent — returns the existing album)
AI: [calls add-to-album album="Trailcam" uuid=[...]]
AI: [calls set-keywords per photo, add=["trailcam"]]
"Filed 34 photos and tagged them 'trailcam' — existing keywords untouched
(set-keywords merges, never replaces)."Fixing Wrong Dates (write tools — dry-run first)
User: "Those trailcam photos are stamped with the upload time, not the capture
time. The strip in the image says 05/14/2026 06:32."
AI: [calls set-photo-date uuid=... date="2026-05-14T06:32:00"] (dryRun defaults to TRUE)
"Preview: 2026-07-09T21:14:03 → 2026-05-14T06:32:00. Apply?"
User: "Yes"
AI: [calls set-photo-date uuid=... date="2026-05-14T06:32:00" dryRun=false]
"Done — and the response echoed the old date, so I can revert if needed."Whole batches with the same clock offset shift with shiftSeconds instead of an absolute date. Only the Photos-library date changes — the file's EXIF is untouched (same as Photos.app's Adjust Date & Time).
Acting on the Photos.app Selection
User: [selects six photos in Photos.app] "Add these to the Yearbook album"
AI: [calls get-selected-photos] → 6 UUIDs
AI: [calls add-to-album album="Yearbook" uuid=[...]]
"Filed the 6 selected photos into Yearbook."Browsing Library Structure
User: "What are my top 10 keywords?"
AI: [calls list-keywords with limit=10]
"Photo Stream (1561), Mollee (109), beach (109), 2015 Feb Keweenaw..."
User: "Who appears most in my photos?"
AI: [calls list-persons with limit=10]
"Rita Sweet (29), Robert B Sweet (28), Jennifer Sweet (24)..."Targeting a Different Library
By default, all operations use the system Photos library. To work with a different .photoslibrary:
User: "Show albums in my old archive at /Volumes/Archive/Photos.photoslibrary"
AI: [calls list-albums with library="/Volumes/Archive/Photos.photoslibrary"]
"32 albums in the archive..."Installation Options
npm (Recommended)
npm install -g apple-photos-mcposxphotos installs automatically on the first tool call — no separate pip3 install needed.
From Source (with Project-Local venv)
git clone https://github.com/sweetrb/apple-photos-mcp.git
cd apple-photos-mcp
pnpm install
pnpm run setup # OPTIONAL — pre-builds ./venv with osxphotos; otherwise it's built on first use
pnpm run buildThe pnpm run setup step is optional: if you skip it, the server auto-bootstraps the venv on the first tool call (one-time, ~a minute). Running it ahead of time just avoids that first-call delay.
You can also install straight from GitHub with npm install -g github:sweetrb/apple-photos-mcp — but this builds from source at install time (requires pnpm), so prefer the registry install above unless you specifically want an unreleased commit.
If installed from source, use this configuration:
{
"mcpServers": {
"apple-photos": {
"command": "node",
"args": ["/path/to/apple-photos-mcp/build/index.js"]
}
}
}The server prefers a project-local venv at ./venv/bin/python3 if present, and otherwise falls back to system python3. If neither has osxphotos, the server auto-builds the venv on first use (unless APPLE_PHOTOS_MCP_NO_AUTO_SETUP=1). The venv is also self-healing: it's picked up as soon as it exists — no server restart needed if you build or repair it while the server is running — and is rebuilt automatically if a package update changes its requirements.
Running from a clone in Claude Code (project-scope .mcp.json)
This repo ships a .mcp.json at its root so that, when you run claude from inside a clone, the server is registered automatically as a project-scope server — no manual config needed. Before launching, you must:
pnpm run build— compile the TypeScript tobuild/index.js.pnpm run setup— optional; pre-builds the project-local venv at./venvwithosxphotos(the server prefers./venv/bin/python3). Skip it and the server builds the venv on the first tool call.Grant Full Disk Access to the app hosting Claude Code (Terminal, iTerm, VS Code, etc.) — the Photos library SQLite is in a protected directory and osxphotos reads it directly. See Full Disk Access.
Then launch Claude Code from the repo directory and approve the server when prompted.
The entrypoint is written as:
"args": ["${CLAUDE_PROJECT_DIR:-.}/build/index.js"]CLAUDE_PROJECT_DIR is the variable Claude Code injects into a project/user-scoped server's environment, and it resolves to the repo root. You must launch claude from inside the repo for this to work — the bare . fallback is only a last resort and is not reliable, because it resolves against the launching process's working directory, not the repo.
Why not
${CLAUDE_PLUGIN_ROOT}?CLAUDE_PLUGIN_ROOTis set only for marketplace plugin installs, never for a project-scope clone, so it can't drive the clone workflow. Conversely, a plugin install can't useCLAUDE_PROJECT_DIR(in a plugin, that points at the user's project, not the plugin's own directory). Claude Code does not support nested defaults like${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PROJECT_DIR:-.}}, so a single entrypoint string cannot serve both contexts. The two distribution paths are therefore decoupled: the plugin carries its own MCP config in.claude-plugin/plugin.json(using${CLAUDE_PLUGIN_ROOT}), while the root.mcp.jsonis dedicated to the clone workflow (using${CLAUDE_PROJECT_DIR:-.}). Becauseplugin.jsondeclares its ownmcpServers, the plugin does not also auto-load the root.mcp.json, so there is no double-registration.
Heads-up on scope precedence: project-scope (
.mcp.json) outranks user-scope. If you also have anapple-photosentry registered at user scope (e.g. an absolute path in~/.claude.json), the project-scope entry wins and the user-scope one is ignored entirely. Pick one — for local development on this repo, the project-scope.mcp.jsonis the intended source. To pin a specific local build instead, register it at local scope (claude mcp add apple-photos -s local -- node /abs/path/build/index.js), which outranks project scope.
Full Disk Access
The Photos library SQLite database lives in a protected directory (~/Pictures/Photos Library.photoslibrary/database/). osxphotos reads this database directly — it does not go through Photos.app — so the host process needs Full Disk Access.
How to Grant Full Disk Access
Open System Settings (or System Preferences on older macOS)
Go to Privacy & Security > Full Disk Access
Click the + button
Add the application that hosts the MCP server:
Claude Desktop: Add
/Applications/Claude.appTerminal: Add
/Applications/Utilities/Terminal.appVS Code: Add
/Applications/Visual Studio Code.appiTerm: Add
/Applications/iTerm.app
Restart the application after granting access
Verifying it worked
Run the doctor tool — it explicitly reports the Full Disk Access check (alongside the Python interpreter version, osxphotos install, and library readability) as ok / warn / fail, so it's the best way to confirm the grant took effect. health-check and library-info also work as a quick smoke test.
For the full why-and-how walkthrough, see the Full Disk Access Setup Guide.
Without Full Disk Access
The health-check tool will fail and report a permissions error, and doctor's Full Disk Access check will report fail. No tool will be able to open the library.
Configuration
All configuration is optional — the server works out of the box.
Environment variables
Variable | Default | Description |
| unset (read-only) | Set to |
|
| Max bytes captured from the Python sidecar's stdout. Raise it if a very large library/query is truncated; lower it to cap memory. |
|
| Default per-command timeout, in milliseconds, for the Python sidecar. The first (cold) call parses the whole Photos database, and on very large libraries (100k+ photos) that load alone can exceed 60 s — raise this if tools report "Operation timed out". It sets only the DEFAULT budget — it applies to |
| unset (persistent mode on) | Set to |
|
| How long the persistent sidecar may sit idle before it's killed to free memory (a resident parsed library holds hundreds of MB for large libraries). The next call transparently respawns it, re-paying the one-time parse. |
| unset (auto-setup on) | Set to |
|
| Max time, in milliseconds, the automatic venv bootstrap may run before it's aborted. Raise it on slow networks where the |
|
| Path to the JSON config file (see below). |
Configuration file (when the host strips env)
Some host apps (e.g. Claude Desktop) launch the MCP server with a scrubbed
environment and ignore the env block in their server config, so there's no way
to pass APPLE_PHOTOS_MCP_* settings through it. In that case, put them in a JSON
file the host doesn't manage — APPLE_PHOTOS_MCP_CONFIG_FILE, or by default
~/Library/Application Support/apple-photos-mcp/config.json:
{
"APPLE_PHOTOS_MCP_MAX_BUFFER": "209715200",
"APPLE_PHOTOS_MCP_ENABLE_WRITES": "1"
}(The second line opts in to the write tools — omit it to keep the server read-only.)
The server reads it at startup and merges values into the environment without
overriding anything already set there (so an explicit env still wins). This
is the recommended way to configure the server under Claude Desktop. Keep only
non-secret config here.
Architecture
This package is a TypeScript MCP server with a Python sidecar:
The MCP server (Node) speaks the Model Context Protocol over stdio. Every tool's
inputSchemaandoutputSchemais advertised as JSON Schema 2020-12 — the dialect MCP standardized on and the only one modern clients will validate against. The MCP SDK's zod converter still emits draft-07, so the outgoingtools/listpayload is normalized at the transport boundary (src/utils/jsonSchemaDialect.ts).A bundled Python script (
src/utils/photos_reader.py) usesosxphotosto read the Photos library and returns JSON.The sidecar runs as a persistent process (
photos_reader.py --serve): the TypeScript side spawns it once on first use and sends it line-delimited JSON requests over stdin, behind a serial gate (exactly one request in flight at a time). The Node event loop stays free, so the server keeps answering MCP traffic (pings,health-check,doctor) even during a longqueryor a minutes-long iCloudexport.If serve mode is unavailable (old script, broken environment), the server transparently falls back to spawning a fresh one-shot Python process per call — same results, same error messages, just slower.
doctor'ssidecar_modecheck reports which mode is active.
Performance
Opening a Photos library is expensive: python startup + import osxphotos + a
full parse of the library database — about 4 seconds on a ~30k-photo
library, and it grows with library size. The persistent sidecar pays that
cost once: the parsed library stays resident, and follow-up calls complete
in milliseconds (measured: ~4.5 s cold, then 6–160 ms warm on a 31k-photo
library). Freshness is preserved — before every request the sidecar checks the
library's Photos.sqlite modification time and re-parses automatically the
moment the library changes (an import, an edit, an album rename). An idle
sidecar is killed after APPLE_PHOTOS_MCP_SIDECAR_IDLE_MS (default 5 min) to
free memory, and the next call respawns it — so the ~4 s cost recurs only on
the first call after a quiet period or a library change.
This is the same TS + Python-sidecar pattern used by apple-numbers-mcp for the numbers-parser Python library.
Security and Privacy
Local only — All operations happen locally via osxphotos. No data is sent to external servers.
Read-only by default — the library is never modified unless you explicitly set
APPLE_PHOTOS_MCP_ENABLE_WRITES=1(see Write tools (opt-in)). Even with writes enabled, the tools are limited to album membership, photo metadata (titles, keywords, favorites, dates), and add-only imports — nothing can delete a photo — and every write requires explicit UUIDs/names/paths (no wildcard operations).Exports write to disk —
exportwrites files to the destination directory you specify, and only into an allowlisted location: the destination must resolve (symlinks included) to a path under your home directory,/tmp,/private/tmp, or/Volumes. Confirm destinations before running on shared machines.No credential storage — The server doesn't store any passwords or authentication tokens.
Known Limitations
For the full rundown — read-only scope, iCloud export caveats, face/album behavior, and library lag — see docs/LIMITATIONS.md. For what query can and cannot filter by (and how), see docs/QUERY-GUIDE.md. The summary below is the quick version.
Limitation | Reason |
macOS only | Apple Photos and osxphotos are macOS-specific |
Read-only by default | osxphotos reads the Photos library directly; the write tools are opt-in ( |
No photo deletion | Deliberate: no tool deletes photos, albums, or folders — quarantine into an album and delete in Photos.app |
| Photos' AppleScript has no remove verb — the album's UUID changes and manual sort order is lost |
Writes target the open library | AppleScript talks to whatever library Photos.app has open — the |
Writes need Automation permission | Driving Photos.app via AppleScript triggers a one-time macOS Automation prompt for the host app |
Full Disk Access required | The Photos library SQLite database is in a protected directory |
iCloud-only export is slower | Originals that aren't on disk are downloaded on demand via Photos.app/AppleScript. The export still succeeds, but takes longer than a local copy and requires Photos.app to be installed and signed in to iCloud |
Photos.app may lock the library | If Photos.app is mid-write, opening the library can fail; close Photos.app and retry |
Person filter requires named faces | osxphotos cannot filter by unnamed/unrecognized faces |
Troubleshooting
The first tool call is slow / "setting up the Python venv" in the logs
This is expected: it's the one-time automatic venv build (creating
./venvand installingosxphotos). It can take ~a minute and logs progress to stderr. Subsequent calls are fast. Pre-warm withpnpm run setupto avoid it.If the build keeps hitting a timeout on a slow network, raise
APPLE_PHOTOS_MCP_SETUP_TIMEOUT(milliseconds; default 5 min).
"osxphotos not installed"
Most common cause: your
python3is older than 3.11. Stock macOS ships Python 3.9, which is too old for the automatic venv setup to succeed. Install a newer Python (brew install python@3.12), then simply retry the tool call — the venv rebuilds automatically.Auto-setup also can't run when
pipor network access is unavailable, or when you setAPPLE_PHOTOS_MCP_NO_AUTO_SETUP=1. Fix the missing piece (or unset the variable) and retry — or install by hand withpip3 install osxphotos(global) orscripts/setup.sh/pnpm run setupfrom a repo checkout (project-local venv).Run the
doctortool for a per-check diagnosis of what's missing.If you used a virtualenv, make sure it's the one at
./venv/in the project directory.
"Library not found" or permission errors
Grant Full Disk Access to the host app — see Full Disk Access.
Verify the library path: default is
~/Pictures/Photos Library.photoslibrary.
Photo not found / "Photo not found: "
The UUID may be wrong — re-run
queryto get current UUIDs.The photo may have been permanently deleted from the library.
exportsays "UUID not found (deleted or in trash)" butget-photoreturns the photo? The photo is in Recently Deleted:get-photofalls back to the trash, whilequeryandexportread the main library only. Restore the photo in Photos.app to export it.
Exports skip files with "missing"
Since 0.1.3, the export auto-downloads iCloud-only originals via Photos.app, so this skip should be rare. If it still happens:
"original not downloaded from iCloud (download attempt returned no files)" — Photos.app couldn't fetch it. Check iCloud connectivity, that you're signed in, and that the photo isn't excluded by a Photos sync setting.
"Photo does not have adjustments..." —
edited=truewas requested but the photo has no edited version. Retry without that flag."raw component not on disk (Photos.app fallback cannot fetch raw originals)" —
raw=truewas requested but the raw file isn't downloaded locally. Retry without the flag, or download the original in Photos.app first (File → Download Originals to this Mac).
"Write tools are disabled — apple-photos-mcp is read-only by default"
Working as designed: the write tools require an explicit opt-in. Set
APPLE_PHOTOS_MCP_ENABLE_WRITES=1in the server's environment or in~/Library/Application Support/apple-photos-mcp/config.json, then restart the MCP server — see Write tools (opt-in).Run the
doctortool to confirm the gate state (itswritescheck reports enabled/disabled and, when enabled, whether the photoscript backend and Photos.app look usable).
Write tools fail with an AppleScript / "not authorized" error
The host app needs macOS Automation permission to control Photos.app. The first write normally triggers a one-time system prompt — click OK. If it was denied, re-enable it under System Settings → Privacy & Security → Automation → (your host app) → Photos, then retry.
In headless contexts (no GUI session) the prompt can't be shown and the write fails with error
-1743; run the first write from a normal GUI session once to grant it.Writes launch Photos.app if it isn't running — the first write after a reboot can take noticeably longer while Photos starts.
Photos.app errors when running
Closing Photos.app may resolve database-lock errors. osxphotos opens the library in read-only mode but still requires that no writer holds an exclusive lock.
Every tool is rejected: "invalid outputSchema … unsupported dialect"
The full message is
Tool '<name>' has an invalid outputSchema: JSON Schema declares an unsupported dialect ("$schema": "http://json-schema.org/draft-07/schema#"). The default validator supports JSON Schema 2020-12 only.The server connects, but no tool is usable.Upgrade to apple-photos-mcp 2.1.10 or later (
npx -y apple-photos-mcp@latest, orpnpm run buildfrom a clone) and restart the host app. Versions up to 2.1.9 advertised draft-07 schemas because the MCP SDK's zod converter emits that dialect; 2.1.10 normalizes every advertised schema to 2020-12.Nothing else changes — no tool, parameter, or result differs between the two dialects for this server.
apple-photos server fails to connect when run from a clone
Launch
claudefrom inside the repo directory soCLAUDE_PROJECT_DIRresolves to the repo root. The bare.fallback resolves against the launching process's working directory, not the repo, and is unreliable.Run
pnpm run buildfirst — the entrypoint${CLAUDE_PROJECT_DIR:-.}/build/index.jswon't exist until you compile.The
./venvwithosxphotosbuilds automatically on the first tool call; runpnpm run setuponly to pre-warm it, or if you've setAPPLE_PHOTOS_MCP_NO_AUTO_SETUP=1.Grant Full Disk Access to the host app (Terminal, iTerm, VS Code, etc.) — see Full Disk Access.
Run
claude mcp listand check for conflicting scopes. Project-scope (.mcp.json) outranks user-scope; a stale user-scopeapple-photosentry pointing at a bad path can mask the project-scope one. To pin a specific build, register it at local scope:claude mcp add apple-photos -s local -- node /abs/path/build/index.js.If the server shows as pending, approve the project-scope server when Claude Code prompts you.
Development
pnpm install # Install dependencies
pnpm run setup # Create ./venv with osxphotos
pnpm run build # Compile TypeScript
pnpm test # Run unit tests
pnpm run test:integration # Run integration tests against the real Photos library
pnpm run test:all # Unit + integration
pnpm run test:coverage # Unit tests with coverage report
pnpm run typecheck # Type-check without emitting
pnpm run lint # Check code style
pnpm run format # Format codeThe Python sidecar is a thin CLI that the TypeScript layer shells out to:
./venv/bin/python3 src/utils/photos_reader.py library-info
./venv/bin/python3 src/utils/photos_reader.py query --keyword sunset --limit 5
./venv/bin/python3 src/utils/photos_reader.py export --uuid <uuid> --dest /tmp/outAuthor
Rob Sweet - President, Superior Technologies Research
A software consulting, contracting, and development company.
Email: rob@superiortech.io
GitHub: @sweetrb
License
MIT License - see LICENSE for details. This project is not affiliated with Apple Inc. or the osxphotos project.
Contributing
Contributions are welcome! Please open an issue or PR at github.com/sweetrb/apple-photos-mcp.
Related Projects
Part of a family of macOS MCP servers:
apple-mail-mcp — MCP server for Apple Mail (read, search, send, and organize email)
apple-notes-mcp — MCP server for Apple Notes (create, search, update, and export notes)
apple-numbers-mcp — MCP server for Apple Numbers (read and write .numbers spreadsheets)
osxphotos — The Python library that powers this server
Recurring macOS permission prompts
If macOS keeps re-prompting for Full Disk Access or Automation for node (often after a brew upgrade), see docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md — the fix is to run this server under the official, Developer-ID-signed Node so the grant survives Node updates.
Available Tools
21 toolsadd-to-albumA
Use when: you have photo UUIDs (from query / find-duplicates) and want to file them into an album — e.g. collecting duplicate extras into a quarantine album, or filing a trip's photos. Returns: the album {uuid, name, path}, addedCount, added (UUIDs newly added), alreadyPresent (UUIDs that were already members — adding is idempotent), and notFound (requested UUIDs that don't exist in the library). Fails only when the album doesn't exist or NO requested photo exists. Do not use when: the album doesn't exist yet — call create-album first; or you want photos OUT of an album — use remove-from-album. Safety: WRITE tool — disabled unless APPLE_PHOTOS_MCP_ENABLE_WRITES=1 (run doctor to check). Changes album membership only: photos are never copied, modified, or deleted, and each target is validated to exist first. Max 100 UUIDs per call. Drives Photos.app via AppleScript (launches it if needed; requires macOS Automation permission — one-time prompt). Writes target the library currently open in Photos.app.
| Name | Required | Description | Default |
|---|---|---|---|
| uuid | Yes | Photo UUID(s) to add (1–100, as returned by query) | |
| album | Yes | Album name or UUID (UUID-looking values try the id lookup first) |
Output Schema
| Name | Required | Description |
|---|---|---|
| added | No | |
| album | No | |
| notFound | No | |
| addedCount | No | |
| alreadyPresent | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries full responsibility and does so thoroughly: it discloses the write nature, the environment variable gate, that photos are never modified/deleted, idempotency, AppleScript launching behavior, Automation permission requirement, and the target library. It even lists error conditions.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is longer than average but every sentence adds value. It is front-loaded with 'Use when', then returns, exclusions, and safety. The structure with labeled paragraphs makes it scannable and informative without redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a write tool with no annotations, this is complete: it covers prerequisites, return values, failure modes, constraints, side effects, and permissions. The output schema exists, and the description augments it with concrete examples and edge-case handling.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3. The description adds meaningful context beyond the schema: where UUIDs come from (query/find-duplicates), the idempotent add behavior, and the album existence requirement. It doesn't fully explain the output mapping but parameters are well contextualized.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool adds photo UUIDs to an album, with concrete examples ('collecting duplicate extras into a quarantine album'). It explicitly distinguishes from siblings by naming remove-from-album for the inverse operation and create-album for missing albums.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It provides explicit 'Use when' conditions (having photo UUIDs from query/find-duplicates) and explicit 'Do not use when' exclusions (album doesn't exist — use create-album first; wanting photos out — use remove-from-album). This is exactly the guidance an agent needs for tool selection.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
create-albumA
Use when: you need an album to file photos into — a new album by name, optionally nested inside a folder path (e.g. for a quarantine album before a dedupe review, or a per-trip album). Returns: album {uuid, name, path} and created — false means an album of that name already existed and was returned instead of creating a duplicate (idempotent: safe to re-run; without folder the name is matched anywhere in the library, with folder only inside that folder). Do not use when: you want to list existing albums — use list-albums; or you want to put photos into the album — follow up with add-to-album. Safety: WRITE tool — disabled unless APPLE_PHOTOS_MCP_ENABLE_WRITES=1 (run doctor to check). Only creates albums/folders; never deletes, moves, or modifies photos. Drives Photos.app via AppleScript: Photos is launched if not running, and macOS Automation permission is required (one-time system prompt on first write). Writes always target the library currently open in Photos.app — there is no library parameter.
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | Album name | |
| folder | No | Folder path to nest the album under, "/"-separated for nesting (e.g. "Trips/2026"); folders are created as needed |
Output Schema
| Name | Required | Description |
|---|---|---|
| album | No | |
| created | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description fully discloses key behavioral traits: it is a WRITE tool requiring an environment variable, it only creates albums/folders, it never destroys or modifies other data, it drives Photos.app via AppleScript, may launch Photos, requires macOS Automation permission, and targets the currently open library. It also explains idempotent behavior and return semantics, providing substantial transparency beyond the schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured into labeled sections (Use when, Returns, Do not use when, Safety) with no wasted words. Each sentence serves a distinct purpose, and the front-loaded use case makes it easy to scan.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description covers use cases, exclusions, return values, safety, side effects, and environmental requirements. Despite having an output schema, it still explains the 'created' flag and idempotent behavior. It is complete for a write tool with this complexity.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% so the baseline is 3, but the description adds meaningful context for the 'name' parameter (matching behavior without folder vs with folder) and clarifies the 'folder' parameter with an example path structure. This goes beyond the schema's brief descriptions, enriching the semantics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose: creating a new album by name, optionally nested in a folder. It distinguishes itself from siblings by explicitly naming list-albums and add-to-album as alternatives for different needs.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit 'Use when' and 'Do not use when' guidance, including concrete examples (quarantine album, per-trip album) and direct references to alternative tools (list-albums, add-to-album). This leaves no ambiguity about when to select this tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
doctorA
Use when: a tool returns a permission, 'unable to open', or 'write tools are disabled' error, or you want a full setup diagnostic before querying, exporting, or writing. Returns: six checks — Python interpreter (path + version; warns below 3.11), osxphotos install, sidecar mode (persistent vs one-shot, plus last respawn), the write-tools gate (enabled/disabled, with the opt-in recipe and — when enabled — whether the photoscript backend and Photos.app look usable), Photos library readability, and Full Disk Access — each reported ok/warn/fail with actionable advice. Do not use when: you only need the lightweight is-it-working smoke test — use health-check instead.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| checks | No | |
| healthy | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are present, so the description carries the full burden. It details six specific checks, the ok/warn/fail reporting, and actionable advice, but does not explicitly state that the tool is non-mutating. The diagnostic nature strongly implies read-only, yet an explicit 'does not modify anything' would strengthen transparency.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured with clear 'Use when', 'Returns', and 'Do not use when' sections. Every sentence earns its place, front-loaded with the most important usage context, and remains concise.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-parameter diagnostic tool with an output schema, the description is complete: it covers when to use, what the six checks are, how results are reported, and the alternative tool. No significant gaps exist.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters, so the description need not explain parameter details. It does add value by explaining what the tool checks and returns, which is a baseline-4 performance for a parameterless tool.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description explicitly states this is a full setup diagnostic for osxphotos, triggered by specific error types, and differentiates from the lightweight health-check sibling by naming it. The verb+resource is implied through 'diagnostic' and the six checks.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit when-to-use ('permission, unable to open, or write tools are disabled' errors, or before querying/exporting/writing) and when-not-to-use ('lightweight is-it-working smoke test — use health-check instead'). Names the alternative sibling tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
exportA
Use when: you want to copy one or more photos (by UUID, typically from query) out to a destination directory on disk. By default exports the original; set edited=true for the edited version, live=true to also include the live-photo video, raw=true to also include the raw image. Large batches report per-photo MCP progress notifications when the request carries a progressToken. Returns: the destination path, counts of files exported and skipped, the exported file paths, and a per-UUID reason for anything skipped (e.g. file already exists at the destination, UUID not found / in trash, iCloud download failed). Do not use when: you only need metadata or file paths rather than copies on disk — use get-photo; or you're still figuring out which photos to export — use query first. Safety: the only side-effecting tool on the read path — it writes files into the destination directory (created if missing); the opt-in write tools (APPLE_PHOTOS_MCP_ENABLE_WRITES=1) can also modify the library, but nothing else here writes anywhere. dest must resolve (after expanding ~ and following symlinks) to a path under your home directory, /tmp, /private/tmp, or /Volumes; anything else is rejected. With overwrite=true it OVERWRITES existing files of the same name in place; without it, existing files are skipped and reported per-UUID. If an original isn't on disk (iCloud 'Optimize Mac Storage'), the export falls back to driving Photos.app via AppleScript to download it on demand — this is slow for large batches and requires Photos.app installed, signed in to iCloud, and Automation permission granted.
| Name | Required | Description | Default |
|---|---|---|---|
| raw | No | Also export the raw image | |
| dest | Yes | Destination directory (created if missing). Must be under the home directory, /tmp, /private/tmp, or /Volumes | |
| live | No | Also export the live-photo video | |
| uuid | Yes | Photo UUID(s) to export | |
| edited | No | Export the edited version instead of the original | |
| library | No | Path to a .photoslibrary (default: system Photos library) | |
| overwrite | No | Overwrite existing files at the destination |
Output Schema
| Name | Required | Description |
|---|---|---|
| skipped | No | |
| exported | No | |
| destination | No | |
| skippedCount | No | |
| exportedCount | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries full burden and delivers richly: it warns this is the only side-effecting tool on the read path, explains overwrite behavior (overwrites vs skips), path restrictions, and the iCloud AppleScript fallback requiring Photos.app and Automation permission. This goes far beyond minimal expectations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Though lengthy, every sentence earns its place. The description is logically structured (use when, returns, do not use when, safety) and front-loads the primary purpose. It packs high-density information without fluff, making the length justified for a tool with this complexity.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description covers all essential operational aspects: return values, skip reasons, file overwrite behavior, path security constraints, iCloud fallback, performance considerations (slow for large batches), and prerequisites. With an output schema present, no further return-value detail is needed. The context is complete for an agent to invoke and trust the tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so baseline is 3. The description adds meaningful nuance beyond schema: 'edited=true for the edited version', 'live=true to also include', 'raw=true to also include', and the default skip/overwrite behavior for existing files. It does not elaborate on 'library', but the schema already sufficiently describes it. Overall, the extra semantics push it above baseline.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description explicitly states the tool copies photos (by UUID) to a destination directory on disk, which is a specific verb+resource+destination. It distinguishes itself from siblings by contrasting with get-photo (metadata/paths only) and query (finding photos), making the purpose unmistakable.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The 'Use when' and 'Do not use when' sections explicitly give usage context and alternatives: use for copying files, avoid for metadata-only needs (use get-photo) or when still identifying photos (use query). This is exactly the kind of clear decision guidance missing in many tool descriptions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
find-duplicatesA
Use when: you want to find exact duplicates across the library — cleaning up after a double import, checking whether files were re-uploaded, or auditing before a migration/export. Returns: groupCount (total duplicate groups found), returned (groups in this response, capped at limit, default 100), and groups ordered newest-first — each with the member UUIDs plus per-member filename, date, size, dimensions, and movie flag. Use get-thumbnail on members to eyeball a group before acting on it. Do not use when: you're looking for near-duplicates or similar shots — Photos' fingerprint matches EXACT duplicates (identical image data) only; edited copies, resized versions, and burst siblings will NOT group. Safety: read-only. This server cannot delete photos — to act on duplicates, quarantine the extra copies into an album (create-album + add-to-album when writes are enabled, otherwise by hand in Photos.app) and review/delete inside Photos.app.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Max duplicate groups to return (default 100; groupCount reports the total) | |
| library | No | Path to a .photoslibrary (default: system Photos library) |
Output Schema
| Name | Required | Description |
|---|---|---|
| groups | No | |
| returned | No | |
| groupCount | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries full burden and meets it thoroughly. It discloses read-only behavior, inability to delete photos, return structure (groupCount vs returned), the 100-group cap default, newest-first ordering, and per-member details. It also warns about the exact-match limitation and explains the recommended review workflow.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured with labeled sections (Use when, Returns, Do not use, Safety) and is appropriately sized given the tool's complexity and absence of annotations. Every sentence earns its place, covering purpose, behavior, limitations, and follow-up actions without redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity and lack of annotations, the description is remarkably complete: it explains use cases, exact-match constraints, return value semantics, safety posture, and manual fallback procedures. The presence of an output schema does not detract from the need for this context, and the description delivers it comprehensively.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, and the schema already documents both parameters, including the limit default and its relationship to groupCount. The description reiterates this information but adds no deeper parameter semantics beyond what the schema provides, so the baseline score of 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool finds exact duplicates across the library, with a specific verb ('find') and resource ('duplicates'). It distinguishes itself from siblings like get-photos or query by emphasizing exact-match semantics and providing concrete use cases such as cleanup after double import and pre-migration auditing.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit 'Use when' and 'Do not use when' sections provide clear conditions for tool selection, including exclusions for near-duplicates and edited copies. It also names complementary tools (get-thumbnail, create-album, add-to-album) and suggests a manual workflow when writes are disabled, giving actionable guidance beyond basic usage.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get-photoA
Use when: you have a single photo's UUID (typically from query) and want its complete metadata. Returns: dimensions and original dimensions, dates, title/description, location and place, albums, keywords, persons, labels, file paths, size, EXIF camera data (make/model, lens, ISO, aperture, shutter speed, focal length — null when Photos recorded none), Photos' ML intelligence (score = overall aesthetic 0–1, detectedText = OCR-indexed text; null on macOS versions without them), iCloud shared-album social data (owner, comments, likes — only populated for shared assets), and type flags (HDR/live/raw/edited/portrait/panorama/etc.). Pass burstPhotos=true to also list the sibling frames of a burst (UUID, filename, date each). Do not use when: you don't have a UUID yet — use query to find matches first; you have several UUIDs — use get-photos for one batched call; or you want to see the image — use get-thumbnail.
| Name | Required | Description | Default |
|---|---|---|---|
| uuid | Yes | Photo UUID (hex-with-dashes, as returned by query) | |
| library | No | Path to a .photoslibrary (default: system Photos library) | |
| burstPhotos | No | true = include burstPhotos: the OTHER frames of this photo's burst set (empty when the photo is not a burst member) |
Output Schema
| Name | Required | Description |
|---|---|---|
| photo | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description discloses detailed behavior: full return list, null handling (EXIF, ML intelligence on older macOS), conditional fields (iCloud social data only for shared assets), and the burstPhotos parameter effect. It also notes the absence of side effects implicitly as a read operation.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is longer than minimal, but the structure is logical: conditions, output details, parameter tip, and exclusions. Each sentence adds necessary information. A slight trim could improve conciseness without losing value, hence not a 5.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity (many return fields, nulls, conditional data) and the presence of an output schema, the description still thoroughly explains return categories and usage constraints. It fully arms the agent with all needed context for correct invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Although schema coverage is 100%, the description adds value: uuid is noted as 'typically from query', and burstPhotos is explained as listing 'sibling frames' and being empty when not a burst member. This enriches the schema descriptions with practical usage nuance.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose: retrieving complete metadata for a single photo identified by UUID. It explicitly contrasts with get-photos (multiple UUIDs) and get-thumbnail (image display), making the resource and action unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description opens with 'Use when' and closes with 'Do not use when', providing explicit conditions and naming alternatives (query, get-photos, get-thumbnail). This gives the AI agent clear decision criteria for tool selection.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get-photosA
Use when: you have SEVERAL UUIDs (typically from query or find-duplicates) and want full metadata for all of them — a dedupe review, an EXIF audit, a captioning pass. One batched sidecar round-trip (max 50 UUIDs) instead of N get-photo calls. Returns: count, photos (full per-photo detail — the same shape as get-photo, including the exif block, score, detectedText, and shared-album owner/comments/likes), and notFound listing any requested UUIDs that matched nothing. Do not use when: you have a single UUID — use get-photo; you don't have UUIDs yet — use query; or you want to see the images — use get-thumbnail per photo.
| Name | Required | Description | Default |
|---|---|---|---|
| uuid | Yes | Photo UUIDs (1–50, as returned by query) | |
| library | No | Path to a .photoslibrary (default: system Photos library) |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | No | |
| photos | No | |
| notFound | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden. It discloses the 50-UUID batch limit, the return structure (count, photos, notFound), and the full per-photo detail shape. It doesn't explicitly state read-only behavior or potential side-effects, but the 'get' verb implies a safe read, and the described return format is transparent.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is moderately long but well-structured with clear 'Use when', 'Returns', and 'Do not use when' sections. Every sentence provides useful guidance; there is no filler or redundancy. Slightly longer than ideal but appropriately so for a batch tool with alternatives and return details.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the output schema exists, the description doesn't need to detail every return field, yet it still summarizes the return shape. It covers usage context, constraints, and alternatives comprehensively. The tool is fully specified for correct selection and invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so both parameters (uuid, library) are already well-documented. The description reinforces that UUIDs come from query/find-duplicates and mentions the 1–50 range, but adds no new semantic details beyond the schema. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's function: fetching full metadata for multiple photo UUIDs in one batched call. It explicitly contrasts with get-photo (single UUID), query, and get-thumbnail, making its purpose distinct and unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit 'Use when' and 'Do not use when' guidance, naming specific alternatives (get-photo, query, get-thumbnail) and conditions (single UUID, no UUIDs yet, viewing images). This leaves no ambiguity about when to invoke this tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get-selected-photosA
Use when: the user says "these photos" / "the selected photos" — they have photos selected in the Photos.app window and you need their identities. This is the GUI-selection bridge: feed the returned UUIDs into get-photos, get-thumbnail, export, or add-to-album. Returns: count, the same photo summaries as query (UUID, filename, date, dimensions, flags), and notFound for selected items the library index doesn't know yet (e.g. a just-finished import Photos hasn't checkpointed — each with its filename for identification). Do not use when: you want to FIND photos by criteria — use query; or Photos.app isn't running / nothing is selected — both return a clear error, and this tool never launches Photos itself. Note: read-only, but it reads the selection from Photos.app via AppleScript, so it requires Photos.app running with a visible selection, and macOS Automation permission for the host app (one-time system prompt on first use). The selection comes from the library currently open in Photos.app.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| count | No | |
| photos | No | |
| notFound | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries full burden. It discloses read-only behavior, reliance on AppleScript, requirement for Photos.app running with a visible selection, macOS Automation permission, and the fact that it never launches Photos itself. It also explains the notFound behavior for uncheckpointed imports.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with the most critical usage guidance, then flows into return values and caveats. Every sentence earns its place, providing dense, non-redundant information. Although it is longer than the calibration example, the extra length is justified by the tool's behavioral complexity and lack of annotations.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given no annotations and an output schema (whose details are not needed because the description summarizes return values), the description fully covers prerequisites, error conditions, permissions, and integration with sibling tools. It leaves no significant gap for an agent to invoke the tool correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters, so the schema is trivially 100% covered. Per rubric, a 0-param tool gets a baseline of 4. The description adds no parameter-specific semantics but appropriately explains the return values and integration points, which is sufficient when no parameters exist.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool reads the user's selected photos from Photos.app, using specific phrases like "GUI-selection bridge" and "returns the same photo summaries as query". This identifies a specific verb-resource pair (get selected photos) and distinguishes it from sibling tools like query, get-photos, and export.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly provides use cases ("Use when: the user says 'these photos'") and exclusions ("Do not use when: you want to FIND photos by criteria — use query"). It also names alternative tools to feed the UUIDs into, giving clear guidance on when to use this tool versus siblings.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get-thumbnailA
Use when: you (or the user) want to SEE a photo — visual triage ('show me…'), picking the best shot, eyeballing duplicate groups, or reading text in an image — without exporting anything to disk. Prefer this over export whenever the goal is to LOOK at a photo rather than to obtain the file. Returns: the photo as an inline MCP image content block (base64 JPEG/PNG a vision-capable client renders directly), plus a text summary and structured metadata (source path, width/height, MIME type, byte size, isDerivative). It serves the smallest Photos-generated preview derivative whose long edge is at least minSize pixels (default 360) — raise minSize (e.g. 1024) when you need detail like small text; isDerivative=false means no suitable derivative existed and the original was downscaled/converted via sips. Do not use when: you need the full-resolution file on disk — use export; or you only need metadata — use get-photo. Movies get a thumbnail only when Photos generated a poster-frame derivative; an iCloud-only photo with no local derivative or original cannot be thumbnailed (export it first, which downloads on demand).
| Name | Required | Description | Default |
|---|---|---|---|
| uuid | Yes | Photo UUID (hex-with-dashes, as returned by query) | |
| library | No | Path to a .photoslibrary (default: system Photos library) | |
| minSize | No | Smallest acceptable long-edge size in pixels (default 360). The smallest qualifying derivative is served, so higher values return larger images |
Output Schema
| Name | Required | Description |
|---|---|---|
| path | No | |
| uuid | No | |
| width | No | |
| height | No | |
| byteSize | No | |
| mimeType | No | |
| isDerivative | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries full burden and thoroughly discloses behavior: how derivatives are selected (smallest with long edge >= minSize), what happens when no derivative exists (downscaled via sips), and the meaning of isDerivative=false. It also covers limitations for movies and iCloud-only photos.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured with clear 'Use when', 'Returns', and 'Do not use when' sections. Every sentence contributes meaningful information without redundancy, and the most important guidance is front-loaded.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity (3 parameters, output schema, edge cases), the description is fully complete. It explains return formats, parameter behaviors, and fallback mechanisms without needing to rely on the output schema, making it self-contained for an agent.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, providing baseline 3. The description adds value by explaining minSize semantics ('raise minSize for detail'), library default (system Photos library), and uuid source (as returned by query), going beyond the schema's basic type descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose: to let the user SEE a photo as an inline preview, distinguishing it from export (full file) and get-photo (metadata). It specifies the exact resource ('photo thumbnail') and the verb ('get-thumbnail') with concrete use cases like visual triage and picking shots.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly provides 'Use when' and 'Do not use when' sections, naming alternatives: use export for full-resolution files, use get-photo for metadata. It also warns about edge cases (movies, iCloud-only) and gives actionable advice (export first).
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
health-checkA
Use when: you want a quick smoke test that osxphotos is installed and the Photos library can be opened. Returns: ok/fail plus the osxphotos version, library path, and total photo count. While another operation (a long query or export) is running, it responds immediately with a liveness summary instead of queueing behind it — re-run after the operation completes for the full result. Do not use when: you need a full setup diagnostic that pinpoints whether the failure is a missing osxphotos, an unreadable library, or denied Full Disk Access — use doctor instead.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| ok | No | |
| message | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries full burden. It discloses the specific return fields (ok/fail, version, library path, photo count) and a non-obvious concurrency behavior (immediate liveness summary instead of queueing). However, it does not mention potential permission requirements or side effects, though it is likely read-only.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is three sentences, with the main purpose in the first sentence, return/concurrency details in the second, and an explicit exclusion in the third. It is front-loaded and every sentence contributes.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description covers the tool's purpose, return values, concurrency behavior, and when to use an alternative. With zero parameters and an output schema present (implied), there are no significant gaps for this simple health-check tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters, so the description cannot add parameter meaning. The baseline of 4 applies as the schema is empty and no parameter documentation is needed.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool performs a quick smoke test for osxphotos installation and library accessibility, using a specific verb and resource. It distinguishes itself from the doctor diagnostic tool by explicitly naming the alternative.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit 'Use when' and 'Do not use when' guidance, with the alternative tool (doctor) named. It also includes a behavioral caveat about re-running after a long operation completes, which informs usage timing.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
import-photosA
Use when: you have image/video files on disk that belong in the Photos library — round-trip edits (export → fix → import), a folder of scans, an SD-card ingest — optionally filed straight into an existing album. Returns: requestedCount (validated source files), importedCount, imported (uuid + filename per new item — feed into get-photos / add-to-album / set-photo-date), and the album when one was targeted. importedCount < requestedCount usually means Photos skipped duplicates. Do not use when: the target album doesn't exist yet — call create-album first (a missing album is an error, not auto-created); or the files are outside your home directory, /tmp, /private/tmp, or /Volumes — those paths are rejected. Safety: WRITE tool — disabled unless APPLE_PHOTOS_MCP_ENABLE_WRITES=1 (run doctor to check). Only ADDS to the library — never modifies or deletes anything; source files stay where they are (Photos copies them in). But note the reverse door is closed: Photos' AppleScript has no photo-delete verb, so an import cannot be programmatically undone — removing a mistaken import requires Photos.app by hand. Every path is validated (absolute, exists, allowed root) before anything imports. Duplicate checking is ON by default; a duplicate then makes Photos.app show a BLOCKING dialog a human must answer (the call waits up to its timeout) — set skipDuplicateCheck=true only when duplicates are acceptable, because they WILL be re-added silently. Drives Photos.app via AppleScript (requires macOS Automation permission; launches Photos if needed). Imports go into the library currently open in Photos.app.
| Name | Required | Description | Default |
|---|---|---|---|
| album | No | EXISTING album (name or UUID) to file the imports into — create it with create-album first if needed | |
| paths | Yes | Absolute (or ~-prefixed) file paths of images/videos to import (1–50). Must exist, under your home directory, /tmp, /private/tmp, or /Volumes | |
| skipDuplicateCheck | No | true = skip Photos' duplicate check: duplicates WILL be re-imported silently. Default false: Photos checks, and a found duplicate raises a blocking dialog in Photos.app that a human must answer |
Output Schema
| Name | Required | Description |
|---|---|---|
| album | No | |
| imported | No | |
| importedCount | No | |
| requestedCount | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden and excels: labels itself a WRITE tool, requires APPLE_PHOTOS_MCP_ENABLE_WRITES=1, notes it only adds to the library (never modifies/deletes), explains the blocking duplicate dialog, mentions AppleScript permissions and that imports go into the currently open library. Also discloses no programmatic undo.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is long but structured with headers ('Use when', 'Returns', 'Do not use when', 'Safety') and every sentence provides unique information. For a tool with this many caveats, the length is justified and front-loaded with the primary use case.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Covers all critical behavioral and operational aspects: environment variable, permissions, path validation, duplicate handling, album requirement, and the lack of undo. The output schema exists, so return values need not be detailed, but the description still mentions them.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so baseline is 3. The description adds context beyond schema by specifying that a missing album is an error rather than auto-created and that every path is validated before import. It also reinforces the duplicate-check semantics, though the schema already covers these, so 4.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with 'Use when: you have image/video files on disk that belong in the Photos library' and clarifies the optional album filing. This clearly states the verb (import) and resource (Photos library), and distinguishes from siblings like export (reverse direction) and create-album (prerequisite for missing albums).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit 'Use when' and 'Do not use when' sections, including the condition that the target album must exist (call create-album first) and that paths must be under allowed roots. Also states the exact environment variable needed to enable the write operation.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
library-infoA
Use when: you want high-level stats about the whole library — total counts of photos, movies, albums, folders, keywords, and persons — or to confirm which library you're targeting before drilling in. Returns: the library path, Photos DB and Photos.app versions, and the six counts. Do not use when: you want the actual albums/keywords/persons rather than just their counts — use list-albums / list-keywords / list-persons; or you want to find specific photos — use query.
| Name | Required | Description | Default |
|---|---|---|---|
| library | No | Path to a .photoslibrary (default: system Photos library) |
Output Schema
| Name | Required | Description |
|---|---|---|
| dbVersion | No | |
| albumCount | No | |
| movieCount | No | |
| photoCount | No | |
| totalCount | No | |
| folderCount | No | |
| libraryPath | No | |
| personCount | No | |
| keywordCount | No | |
| photosVersion | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden. It clearly discloses the return value (library path, versions, six counts) and implies a read-only operation. It doesn't discuss errors or performance, but for a stats-gathering tool this is acceptable.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is appropriately sized, front-loaded with the 'Use when' clause, and structured into clear segments (Use when, Returns, Do not use when). Every sentence adds value with no redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The tool is simple with one optional parameter, has an output schema, and the description covers its purpose, usage boundaries, and return value. It is complete for an agent to select and invoke correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The single parameter 'library' is fully documented in the schema (path, default), and schema coverage is 100%. The description adds no extra parameter semantics, so the baseline of 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states that the tool provides high-level stats about the whole library, including total counts and version information. It explicitly names the resources (library, counts, versions) and distinguishes itself from sibling tools like list-albums and query.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit 'Use when' and 'Do not use when' guidance, including concrete alternative tools (list-albums, list-keywords, list-persons, query) for different use cases. This fully helps an agent decide when to invoke this tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list-albumsA
Use when: you want the catalog of albums — e.g. to discover exact album names before filtering query by album, or to browse the library's organization. Returns: every album's title, folder path, photo count, shared status, and UUID. Do not use when: you want the photos inside an album — use query with the album filter; you want the folder hierarchy rather than albums — use list-folders; or you just want a total album count — use library-info.
| Name | Required | Description | Default |
|---|---|---|---|
| library | No | Path to a .photoslibrary (default: system Photos library) |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | No | |
| albums | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden. It discloses the return fields (title, folder path, photo count, shared status, UUID) and clearly scopes the tool to reading album metadata, implying a non-destructive purpose. However, it doesn't mention edge cases or performance, but for a simple list tool this is adequate.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is extremely concise, broken into three clear parts: when to use, what it returns, and when not to use. Every sentence carries value, and the critical guidance is front-loaded.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
This is a simple list tool with one optional parameter and an output schema (present). The description covers purpose, usage, alternatives, and return values. No additional information is necessary for an agent to use it effectively.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% — the 'library' parameter has a full description ('Path to a .photoslibrary (default: system Photos library)'). The tool description adds no additional parameter context, so baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description starts with 'Use when: you want the catalog of albums' clearly stating the purpose. It distinguishes from siblings by explicitly naming alternatives like query, list-folders, and library-info for other use cases.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly says when to use ('to discover exact album names before filtering...' or 'to browse the library's organization') and when not to use, with specific alternative tools and use cases.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list-foldersA
Use when: you want the library's folder hierarchy — the containers that hold albums and subfolders — to understand how albums are nested. Returns: every folder's title, parent folder, album count, and subfolder count. Do not use when: you want the albums themselves (with their photo counts) — use list-albums; or you just want a total folder count — use library-info.
| Name | Required | Description | Default |
|---|---|---|---|
| library | No | Path to a .photoslibrary (default: system Photos library) |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | No | |
| folders | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden. It discloses what the tool returns (each folder's title, parent folder, album count, subfolder count), implying a read-only query. It does not explicitly state 'does not modify anything', but the 'Returns' phrasing and list-oriented purpose make the behavior sufficiently clear.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is three concise sentences, each earning its place: when to use, what it returns, and when not to use. It is front-loaded with the usage intent and contains no filler words.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple tool with one optional parameter, an output schema, and clear sibling context, the description covers purpose, usage, return content, and exclusions. It is complete enough for an agent to invoke it correctly without further information.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% (the 'library' parameter is described in the schema). The description does not add new meaning beyond mentioning 'library's folder hierarchy', which is already implied. Baseline 3 applies as schema does the heavy lifting.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool lists the library's folder hierarchy, specifying the containers (folders) that hold albums and subfolders. It distinguishes from siblings like list-albums and library-info by specifying what this tool does not return, ensuring no ambiguity.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit 'Use when' and 'Do not use when' sections provide clear context and explicitly name alternatives (list-albums, library-info). This goes beyond mere implication, giving the agent direct decision rules.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list-keywordsA
Use when: you want the catalog of keywords (tags) in the library — e.g. to discover exact keyword spellings before filtering query by keyword, or to see which tags are most used. Pass limit for the top-N. Returns: keywords with their photo counts, sorted most-used first. Do not use when: you want photos carrying a keyword — use query with the keyword filter; or you want people/faces rather than tags — use list-persons.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Top-N keywords | |
| library | No | Path to a .photoslibrary (default: system Photos library) |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | No | |
| keywords | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden. It discloses the return format (keywords with photo counts), ordering (most-used first), and the optional limit parameter. While it doesn't mention pagination or authorization, these are not critical for a simple read-only catalog tool, and the description provides sufficient behavioral context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is three concise sentences, each serving a distinct purpose: usage context, return details, and exclusions. There is no redundant or filler content.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple list tool with an output schema and fully described parameters, the description covers all necessary context: when to use, what it returns, and related alternatives. No significant gaps remain.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3. The description mentions 'limit' as 'top-N', but the schema already provides that same description. It adds no new meaning beyond the schema, which is acceptable given full schema coverage.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool lists keyword tags in the library and returns them with photo counts, sorted by frequency. It explicitly differentiates from sibling tools like query and list-persons by specifying what it does NOT do.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit 'Use when' and 'Do not use when' sections, naming alternative tools and specific use cases (e.g., discovering exact spellings before filtering, or getting top-N tags). This gives unambiguous guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list-personsA
Use when: you want the catalog of named people from Photos face recognition — e.g. to discover exact person names before filtering query by person, or to see who appears most. Pass limit for the top-N; unidentified faces appear as UNKNOWN. Returns: persons with their photo counts, sorted most-photographed first. Do not use when: you want photos of a person — use query with the person filter; or you want subject tags rather than people — use list-keywords.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Top-N persons | |
| library | No | Path to a .photoslibrary (default: system Photos library) |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | No | |
| persons | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden. It discloses that unidentified faces appear as _UNKNOWN_ and that results are sorted by photo count, adding behavioral detail beyond the schema. It doesn't explicitly mention read-only status, but for a list operation this is a minor omission.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description uses a structured format with 'Use when', 'Returns', and 'Do not use when' headers, front-loading the purpose. Every sentence contributes, and there is no wasted wording.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple list operation with an output schema, the description covers the use case, return format, behavioral quirks (_UNKNOWN_), and exclusions. The tool is fully contextualized for an agent to select and invoke correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so baseline is 3. The description adds 'Pass limit for the top-N' which reinforces the limit parameter's purpose, but it doesn't meaningfully expand on the library parameter beyond what the schema already states.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool returns a catalog of named people from Photos face recognition, with specific use cases like discovering person names and seeing photo counts. It distinguishes from sibling tools by referencing the person filter in query and list-keywords.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit 'Use when' and 'Do not use when' sections, naming exact alternative tools (query with person filter, list-keywords) and describing appropriate contexts, such as discovering exact names before querying.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
queryA
Use when: you need to find photos matching one or more filters — album, keyword, person, ML label, place, GPS radius (near), folder, taken-date or import-date range (addedAfter/addedInLast for 'recently imported'), year, file size, media type (screenshot, screen recording, selfie, panorama, live, portrait, time-lapse, slow-mo, burst, video), aesthetic score (minScore), OCR-detected text (detectedText), favorite/hidden flags, or title/description substrings — and get back a list of matches. This is the primary search/discovery tool; start here when you don't already have a UUID. Hidden photos are excluded unless hidden=true. Pass newestFirst=true with a limit to get the N most recent matches. Returns: count (the TOTAL number of matches), returned (the number of summaries in this response — capped at limit, default 500), and photo summaries (UUID, filename, date, dimensions, favorite/hidden/movie flags) — feed a UUID into get-photo/get-photos for full metadata, get-thumbnail to see it, or export to copy files. Do not use when: you already have UUIDs and want full metadata — use get-photo / get-photos; you want to see an image — use get-thumbnail; or you just want the catalog of album/keyword/person names — use list-albums / list-keywords / list-persons.
| Name | Required | Description | Default |
|---|---|---|---|
| live | No | Only live photos | |
| near | No | GPS-radius filter: "lat,lon,radiusKm" — only photos within radiusKm of the point (great-circle distance). Composes (AND) with every other filter. Requires location data: photos without GPS coordinates never match | |
| uuid | No | Specific UUIDs to fetch | |
| year | No | Taken in calendar year(s); ANY-match (e.g. [2024, 2025]) | |
| album | No | Album name(s); ANY-match | |
| burst | No | Only burst photos | |
| label | No | ML classification label(s) from Photos object detection (the labels field of get-photo, e.g. Dog, Beach, Text); ANY-match, exact whole-string | |
| limit | No | Cap the number of results returned (default 500 when omitted; count still reports the total matches) | |
| place | No | Place-name substring(s) from reverse geocoding (city, region, landmark). NOTE: multiple values are ANDed, not ORed — a photo must match every value | |
| title | No | Substring match on title | |
| video | No | Only videos/movies (alias of movies) | |
| folder | No | Folder name(s)/path(s) — matches photos in albums that live inside the folder; ANY-match (see list-folders for names) | |
| hidden | No | Only hidden photos | |
| movies | No | Include movies | |
| person | No | Person name(s); ANY-match | |
| photos | No | Include still photos | |
| selfie | No | Only selfies (front-camera photos) | |
| slowMo | No | Only slow-motion videos | |
| toDate | No | ISO 8601 upper bound on photo date. A bare date (e.g. 2025-06-30) includes that whole day; pass a full datetime (e.g. 2025-06-30T18:00:00) for a precise exclusive bound | |
| keyword | No | Keyword(s); ANY-match | |
| library | No | Path to a .photoslibrary (default: system Photos library) | |
| maxSize | No | Original file size at most this many bytes | |
| minSize | No | Original file size at least this many bytes | |
| favorite | No | Only favorites | |
| fromDate | No | ISO 8601 lower bound on photo date | |
| minScore | No | Only photos whose Photos-computed overall aesthetic score (0–1) is at least this — e.g. 0.7 for 'the good ones'. Post-filter over the other filters' matches; photos without a computed score never match | |
| panorama | No | Only panoramas | |
| portrait | No | Only portrait-mode (depth-effect) photos | |
| noKeyword | No | Only photos carrying no keyword at all | |
| notHidden | No | Exclude hidden photos (default behavior) | |
| timelapse | No | Only time-lapse videos | |
| addedAfter | No | ISO 8601 inclusive lower bound on IMPORT date (dateAdded — when the photo entered the library, not when it was taken) | |
| screenshot | No | Only screenshots | |
| addedBefore | No | ISO 8601 upper bound on IMPORT date. A bare date includes that whole day; a full datetime is a precise exclusive bound | |
| addedInLast | No | Imported within the trailing duration — "<number><unit>", unit s(econds) / m(inutes) / h(ours) / d(ays) / w(eeks), e.g. "7d" or "24h". The natural way to express "recently imported" | |
| description | No | Substring match on description | |
| hasLocation | No | true = only photos WITH GPS coordinates; false = only photos WITHOUT; omit for no location filter | |
| newestFirst | No | Sort matches by taken date, newest first, BEFORE limit is applied — so limit means 'the N most recent matches' instead of 'N in database order' | |
| notFavorite | No | Exclude favorites | |
| detectedText | No | Case-insensitive substring match over the text Photos' own OCR indexed in each photo (macOS 13+) — receipts, signs, screenshots. Post-filter that reads per-photo search info over every other filter's matches, so combine it with narrowing filters (dates, album) on big libraries | |
| screenRecording | No | Only screen recordings |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | No | |
| photos | No | |
| returned | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full transparency burden and does it thoroughly. It discloses default exclusions (hidden photos unless hidden=true), count vs. returned semantics, newestFirst ordering behavior, post-filter effects for minScore and detectedText, AND/OR semantics for place vs. other filters, and the requirement for GPS data with near. No contradictions with annotations since none exist.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is long but highly structured with clear sections (Use when, Returns, Do not use when). Every sentence provides unique, actionable guidance for a complex 41-parameter tool, and the front-loaded use case ensures immediate understanding. The length is justified by the tool's scope.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity, the description is remarkably complete: it covers purpose, scope, filtering capabilities, return value semantics, exclusions, and clear alternatives. The presence of an output schema and full parameter documentation in the schema complements the description, so nothing essential is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Even though the schema has 100% coverage, the description adds significant semantic value beyond individual parameter descriptions. It groups filter categories, explains ANY-match vs. ANDed behavior, defines post-filter operations, gives natural-language examples (e.g., "recently imported" as addedInLast), and clarifies return fields. This goes well beyond the schema baseline.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with "Use when: you need to find photos matching one or more filters" and explicitly names the resource (photos) and the action (search/query). It distinguishes itself from siblings by stating "This is the primary search/discovery tool; start here when you don't already have a UUID" and by contrasting with get-photo/get-photos, get-thumbnail, and list-* tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit "Use when" and "Do not use when" sections listing precise alternatives: use get-photo/get-photos for UUID-based metadata, get-thumbnail for viewing images, and list-albums/list-keywords/list-persons for catalog names. This gives the agent unambiguous selection criteria.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
remove-from-albumA
Use when: you want to take photos OUT of an album — undoing a mis-filing, or clearing reviewed items from a quarantine album. This removes ALBUM MEMBERSHIP only.
Returns: the album AFTER the operation ({uuid, name, path} — note the uuid CHANGES when anything was removed), removedCount, removed, notInAlbum (requested UUIDs that weren't members — no-ops), albumRecreated, and previousAlbumUuid.
Do not use when: you want to delete photos from the library — this server cannot delete photos at all (quarantine them in an album and review in Photos.app instead); or the photos aren't in the album (harmless, but pointless).
Safety: WRITE tool — disabled unless APPLE_PHOTOS_MCP_ENABLE_WRITES=1 (run doctor to check). NEVER deletes photos from the library — removed photos stay in All Photos and every other album. Photos' AppleScript has no remove-from-album verb, so the album is REBUILT (same name and remaining photos): its UUID changes and any custom manual sort order is lost; re-fetch the album UUID from the response. When none of the UUIDs are members, nothing is rebuilt. The replacement is built under a scratch name apple-photos-mcp-tmp-<hex> and renamed last, so a call killed mid-rebuild (e.g. the 10-minute budget expiring while copying a very large album) leaves the original album and every photo intact but can strand an apple-photos-mcp-tmp-… album to delete in Photos.app; each interrupted run strands a distinct one. Max 100 UUIDs per call. Drives Photos.app via AppleScript (requires macOS Automation permission). Writes target the library currently open in Photos.app.
| Name | Required | Description | Default |
|---|---|---|---|
| uuid | Yes | Photo UUID(s) to remove from the album (1–100) | |
| album | Yes | Album name or UUID (UUID-looking values try the id lookup first) |
Output Schema
| Name | Required | Description |
|---|---|---|
| album | No | |
| removed | No | |
| notInAlbum | No | |
| removedCount | No | |
| albumRecreated | No | |
| previousAlbumUuid | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description fully carries the behavioral disclosure burden. It covers write-enablement (APPLE_PHOTOS_MCP_ENABLE_WRITES=1), the rebuild behavior with UUID change and sort order loss, scratch album naming, interruption safety, max 100 UUIDs, and AppleScript permissions—far beyond what annotations would provide.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is dense but front-loaded with the key 'Use when' guidance. Every sentence adds critical operational detail; however, it runs long and could be tightened without losing essential information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description covers return values (album after operation, removedCount, removed, notInAlbum, albumRecreated, previousAlbumUuid), failure modes (interrupted rebuild, stranded temp album), and library context (cannot delete photos). This is complete for a complex write tool with no annotations and a rich output schema.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3. The description adds meaningful semantics beyond the schema: album accepts name or UUID with the UUID-looking-first resolution order, and the response semantics (notInAlbum, albumRecreated) clarify edge cases. This pushes it a point above baseline.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb (remove) with a clear resource (album membership) and explicitly contrasts with deletion ('Do not use when: you want to delete photos from the library'). It also distinguishes from siblings like add-to-album and set-photo-metadata by stating 'removes ALBUM MEMBERSHIP only'.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives explicit 'Use when' and 'Do not use when' conditions, including a concrete alternative (quarantine in an album and review in Photos.app). This directly addresses tool selection vs. alternatives and clarifies when the tool is inappropriate (photos not in album).
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
set-keywordsA
Use when: you want to add and/or remove keywords (tags) on a photo — tagging workflows, fixing a mis-tag — without disturbing its other keywords. Returns: uuid, before/after keyword lists (revert by re-running with the diff inverted), added and removed (what actually changed — adding an existing keyword or removing an absent one is a no-op), and changed. Do not use when: you want to browse keywords — use list-keywords; or find photos by keyword — use query. A keyword passed in both add and remove is rejected. Safety: WRITE tool — disabled unless APPLE_PHOTOS_MCP_ENABLE_WRITES=1 (run doctor to check). UNION semantics — the photo's current keywords are read first and edits are merged in, so existing keywords you don't mention are ALWAYS preserved (never a blind replace). Metadata only — the image asset is untouched; the target photo is validated to exist first. Drives Photos.app via AppleScript (requires macOS Automation permission). Writes target the library currently open in Photos.app.
| Name | Required | Description | Default |
|---|---|---|---|
| add | No | Keywords to add (created in Photos if new) | |
| uuid | Yes | Photo UUID (hex-with-dashes, as returned by query) | |
| remove | No | Keywords to remove from this photo (exact match) |
Output Schema
| Name | Required | Description |
|---|---|---|
| uuid | No | |
| added | No | |
| after | No | |
| before | No | |
| changed | No | |
| removed | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations present, the description carries the full burden and excels. It discloses the write nature, environment dependency (APPLE_PHOTOS_MCP_ENABLE_WRITES), the union/merge semantics ensuring preservation of unmentioned keywords, metadata-only impact, asset validation, AppleScript permission requirements, and the target library context. All critical behavioral aspects are transparent.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Despite its length, the description is exceptionally well-structured with clear prefixed sections ('Use when:', 'Returns:', 'Do not use when:', 'Safety:') that make scanning easy. Every sentence provides unique, essential information, and the structure front-loads the primary purpose and use cases.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's write nature, the description covers safety, permissions, library context, validation, and return values. With an output schema present, it still explains what the returned diff means and how to revert. It is fully complete for an agent to safely and correctly invoke the tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema already has 100% description coverage for the three parameters. The description adds valuable semantics beyond the schema, such as the no-op behavior for adding existing or removing absent keywords, the rejection of overlapping add/remove, and the effect of new keyword creation. This enriches the schema's basic field descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's function: 'add and/or remove keywords (tags) on a photo'. It also distinguishes this tool from sibling tools by emphasizing the non-destructive merge semantics ('without disturbing its other keywords') and explicitly contrasting with list-keywords and query in the 'Do not use when' section.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit use cases ('tagging workflows, fixing a mis-tag'), explicit exclusions ('Do not use when: you want to browse keywords — use list-keywords; or find photos by keyword — use query'), and an additional constraint ('A keyword passed in both add and remove is rejected'). This fully guides an agent on when and when not to use the tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
set-photo-dateA
Use when: a photo's date/time is wrong and you want to fix it — trailcam or scanner imports stamped with the upload time, a camera with a mis-set clock, scanned prints. Set an absolute date OR shift by a number of seconds (exactly one of date / shiftSeconds). DRY RUN BY DEFAULT: with dryRun omitted (or true) it only reports the current and would-be dates — preview first, then re-run with dryRun=false to write. Returns: uuid, before and after datetimes (on a dry run, after = the would-be date), shiftSeconds (the effective delta), applied, and dryRun. Revert an applied change by re-running with date= and dryRun=false. Do not use when: you want to find photos by date — use query; or you expect the file's EXIF to change — this edits the Photos library date only. Safety: WRITE tool — disabled unless APPLE_PHOTOS_MCP_ENABLE_WRITES=1 (run doctor to check). Rewrites the photo's date in the Photos LIBRARY DATABASE only — the same operation as Photos.app's 'Adjust Date & Time'; the original file's EXIF is never modified. Dates are interpreted in the Mac's local timezone (a timezone-aware ISO datetime is converted to local). Nothing is written unless dryRun=false is passed explicitly, and before/after are always echoed so any change can be reverted. The target photo is validated to exist first. Drives Photos.app via AppleScript (requires macOS Automation permission). Writes target the library currently open in Photos.app.
| Name | Required | Description | Default |
|---|---|---|---|
| date | No | Absolute new date-time, ISO 8601 (e.g. 2026-05-14T06:32:00), interpreted in the Mac's local timezone unless a UTC offset is included. Exactly one of date / shiftSeconds | |
| uuid | Yes | Photo UUID (hex-with-dashes, as returned by query) | |
| dryRun | No | Default TRUE: preview the before/after dates without writing anything. Pass false to actually write the new date | |
| shiftSeconds | No | Shift the current date by this many seconds (negative = earlier; e.g. -86400 = one day back). Exactly one of date / shiftSeconds |
Output Schema
| Name | Required | Description |
|---|---|---|
| uuid | No | |
| after | No | |
| before | No | |
| dryRun | No | |
| applied | No | |
| shiftSeconds | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description fully discloses behavior: dry-run default, write operation, safety guardrails, timezone handling, EXIF untouched, validation, AppleScript permissions, and library targeting. It even explains the revert mechanism, giving complete transparency.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Though lengthy, every sentence earns its place. The description is well-organized with clear sections (Use when, Do not use when, Safety, Returns) and front-loaded with the core action. It avoids redundancy and is appropriately detailed for a write tool with safety implications.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity (write operation, dry-run, timezone, safety), the description is exceptionally complete. It covers prerequisites, side effects, return values, revert strategy, and exclusions, all without relying on annotations or an output schema. It answers virtually any question an agent might have.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so baseline is 3. The description adds minimal extra parameter meaning beyond the schema, only reiterating the mutual exclusivity of date/shiftSeconds and the dryRun default. It includes practical usage context like preview-first, but that's more usage guidance than parameter semantics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description explicitly states the tool's function: fixing a photo's date/time by setting an absolute date or shifting by seconds. It clearly distinguishes from sibling tools by saying 'Do not use when: you want to find photos by date — use query' and notes it only edits the Photos library date, not EXIF.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides detailed 'Use when' scenarios (trailcam/scanner imports, mis-set clock, scanned prints) and explicit 'Do not use when' exclusions with alternatives (e.g., query for finding photos by date). Also explains the dry-run-first workflow and safety prerequisites like APPLE_PHOTOS_MCP_ENABLE_WRITES=1.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
set-photo-metadataA
Use when: you want to set a photo's title, description, or favorite flag — captioning passes, marking the best shot of a burst, titling scans. Returns: uuid, updated (which fields were written), and the full before/after values of all three fields — so any change can be reverted by writing the before values back. Do not use when: you want keywords — use set-keywords (it has union semantics; this tool doesn't touch keywords); or you only want to READ metadata — use get-photo. Safety: WRITE tool — disabled unless APPLE_PHOTOS_MCP_ENABLE_WRITES=1 (run doctor to check). Metadata only — never touches the image asset, and only the fields you pass are modified (an empty string clears title/description). The target photo is validated to exist first. Drives Photos.app via AppleScript (requires macOS Automation permission). Writes target the library currently open in Photos.app.
| Name | Required | Description | Default |
|---|---|---|---|
| uuid | Yes | Photo UUID (hex-with-dashes, as returned by query) | |
| title | No | New title (empty string clears it) | |
| favorite | No | Set or clear the favorite flag | |
| description | No | New description (empty string clears it) |
Output Schema
| Name | Required | Description |
|---|---|---|
| uuid | No | |
| after | No | |
| before | No | |
| updated | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description fully discloses behavior: it is a WRITE tool requiring an env var, only modifies passed fields, never touches the image asset, validates photo existence, requires macOS Automation permission, and targets the open Photos library. This exceeds typical transparency.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured with labeled sections (Use when, Returns, Do not use when, Safety). It is dense but every sentence adds value—no filler. Front-loading the purpose and usage makes it highly scannable.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Despite the tool's complexity (write operation, safety flags, permissions, validation), the description covers all essential context: prerequisites, side effects, exclusions, return values, and operational constraints. Output schema info is supplemented by the description of returned fields.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so baseline is 3. The description adds beyond schema by explaining that empty string clears title/description, favorite is a boolean flag, and only passed fields are modified. This enriches parameter understanding beyond the schema's field descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose: 'set a photo's title, description, or favorite flag'. It names specific resources and actions, and explicitly distinguishes from siblings ('Do not use when: you want keywords — use set-keywords; or read metadata — use get-photo').
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives explicit usage guidance: 'Use when' lists concrete scenarios, and 'Do not use when' names alternative tools for exclusions. It also mentions prerequisites like the APPLE_PHOTOS_MCP_ENABLE_WRITES environment variable.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
21 tool updates
v2.1.9- Changed
add-to-album1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
create-album1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
doctor1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
export1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
find-duplicates1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
get-photo1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
get-photos1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
get-selected-photos1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
get-thumbnail1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
health-check1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
import-photos1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
library-info1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
list-albums1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
list-folders1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
list-keywords1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
list-persons1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
query1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
remove-from-album1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
set-keywords1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
set-photo-date1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
- Changed
set-photo-metadata1 field changed- changed
Output schema / additionalPropertiesPrevious value: -falseNew value: +true
5 tool updates
v2.1.0- Changed
get-photo1 field changed- added
Input schema / properties / burstPhotosAdded value: +{ + "description": "true = include burstPhotos: the OTHER frames of this photo's burst set (empty when the photo is not a burst member)", + "type": "boolean" +}
- Added
get-selected-photos - Added
import-photos - Changed
query3 fields changed- added
Input schema / properties / detectedTextAdded value: +{ + "description": "Case-insensitive substring match over the text Photos' own OCR indexed in each photo (macOS 13+) — receipts, signs, screenshots. Post-filter that reads per-photo search info over every other filter's matches, so combine it with narrowing filters (dates, album) on big libraries", + "maxLength": 256, + "minLength": 1, + "type": "string" +} - added
Input schema / properties / minScoreAdded value: +{ + "description": "Only photos whose Photos-computed overall aesthetic score (0–1) is at least this — e.g. 0.7 for 'the good ones'. Post-filter over the other filters' matches; photos without a computed score never match", + "maximum": 1, + "minimum": 0, + "type": "number" +} - added
Input schema / properties / nearAdded value: +{ + "description": "GPS-radius filter: \"lat,lon,radiusKm\" — only photos within radiusKm of the point (great-circle distance). Composes (AND) with every other filter. Requires location data: photos without GPS coordinates never match", + "maxLength": 128, + "pattern": "^\\s*-?\\d+(\\.\\d+)?\\s*,\\s*-?\\d+(\\.\\d+)?\\s*,\\s*\\d+(\\.\\d+)?\\s*$", + "type": "string" +}
- Added
set-photo-date
5 tool updates
v2.0.0- Added
add-to-album - Added
create-album - Added
remove-from-album - Added
set-keywords - Added
set-photo-metadata
4 tool updates
v1.5.0- Added
find-duplicates - Added
get-photos - Added
get-thumbnail - Changed
query22 fields changed- added
Input schema / properties / addedAfterAdded value: +{ + "description": "ISO 8601 inclusive lower bound on IMPORT date (dateAdded — when the photo entered the library, not when it was taken)", + "maxLength": 64, + "type": "string" +} - added
Input schema / properties / addedBeforeAdded value: +{ + "description": "ISO 8601 upper bound on IMPORT date. A bare date includes that whole day; a full datetime is a precise exclusive bound", + "maxLength": 64, + "type": "string" +} - added
Input schema / properties / addedInLastAdded value: +{ + "description": "Imported within the trailing duration — \"<number><unit>\", unit s(econds) / m(inutes) / h(ours) / d(ays) / w(eeks), e.g. \"7d\" or \"24h\". The natural way to express \"recently imported\"", + "maxLength": 32, + "pattern": "^\\s*\\d+(\\.\\d+)?\\s*[smhdw]\\s*$", + "type": "string" +} - added
Input schema / properties / burstAdded value: +{ + "description": "Only burst photos", + "type": "boolean" +} - added
Input schema / properties / folderAdded value: +{ + "description": "Folder name(s)/path(s) — matches photos in albums that live inside the folder; ANY-match (see list-folders for names)", + "items": { + "maxLength": 1024, + "type": "string" + }, + "maxItems": 100, + "type": "array" +} - added
Input schema / properties / hasLocationAdded value: +{ + "description": "true = only photos WITH GPS coordinates; false = only photos WITHOUT; omit for no location filter", + "type": "boolean" +} - added
Input schema / properties / labelAdded value: +{ + "description": "ML classification label(s) from Photos object detection (the labels field of get-photo, e.g. Dog, Beach, Text); ANY-match, exact whole-string", + "items": { + "maxLength": 1024, + "type": "string" + }, + "maxItems": 100, + "type": "array" +} - added
Input schema / properties / liveAdded value: +{ + "description": "Only live photos", + "type": "boolean" +} - added
Input schema / properties / maxSizeAdded value: +{ + "description": "Original file size at most this many bytes", + "exclusiveMinimum": 0, + "type": "integer" +} - added
Input schema / properties / minSizeAdded value: +{ + "description": "Original file size at least this many bytes", + "exclusiveMinimum": 0, + "type": "integer" +} - added
Input schema / properties / newestFirstAdded value: +{ + "description": "Sort matches by taken date, newest first, BEFORE limit is applied — so limit means 'the N most recent matches' instead of 'N in database order'", + "type": "boolean" +} - added
Input schema / properties / noKeywordAdded value: +{ + "description": "Only photos carrying no keyword at all", + "type": "boolean" +} - added
Input schema / properties / panoramaAdded value: +{ + "description": "Only panoramas", + "type": "boolean" +} - added
Input schema / properties / placeAdded value: +{ + "description": "Place-name substring(s) from reverse geocoding (city, region, landmark). NOTE: multiple values are ANDed, not ORed — a photo must match every value", + "items": { + "maxLength": 1024, + "type": "string" + }, + "maxItems": 100, + "type": "array" +} - added
Input schema / properties / portraitAdded value: +{ + "description": "Only portrait-mode (depth-effect) photos", + "type": "boolean" +} - added
Input schema / properties / screenRecordingAdded value: +{ + "description": "Only screen recordings", + "type": "boolean" +} - added
Input schema / properties / screenshotAdded value: +{ + "description": "Only screenshots", + "type": "boolean" +} - added
Input schema / properties / selfieAdded value: +{ + "description": "Only selfies (front-camera photos)", + "type": "boolean" +} - added
Input schema / properties / slowMoAdded value: +{ + "description": "Only slow-motion videos", + "type": "boolean" +} - added
Input schema / properties / timelapseAdded value: +{ + "description": "Only time-lapse videos", + "type": "boolean" +} - added
Input schema / properties / videoAdded value: +{ + "description": "Only videos/movies (alias of movies)", + "type": "boolean" +} - added
Input schema / properties / yearAdded value: +{ + "description": "Taken in calendar year(s); ANY-match (e.g. [2024, 2025])", + "items": { + "maximum": 9999, + "minimum": 0, + "type": "integer" + }, + "maxItems": 100, + "type": "array" +}
2 tool updates
v1.4.0- Changed
export2 fields changed- changed
Input schema / properties / dest / descriptionPrevious value: -"Destination directory (created if missing)"New value: +"Destination directory (created if missing). Must be under the home directory, /tmp, /private/tmp, or /Volumes" - added
Input schema / properties / dest / minLengthAdded value: +1
- Changed
get-photo3 fields changed- changed
Input schema / properties / uuid / descriptionPrevious value: -"Photo UUID"New value: +"Photo UUID (hex-with-dashes, as returned by query)" - added
Input schema / properties / uuid / maxLengthAdded value: +256 - added
Input schema / properties / uuid / patternAdded value: +"^[0-9A-Fa-f-]+$"
1 tool update
v1.2.0- Changed
query3 fields changed- changed
Input schema / properties / limit / descriptionPrevious value: -"Cap the number of results"New value: +"Cap the number of results returned (default 500 when omitted; count still reports the total matches)" - changed
Input schema / properties / toDate / descriptionPrevious value: -"ISO 8601 upper bound on photo date"New value: +"ISO 8601 upper bound on photo date. A bare date (e.g. 2025-06-30) includes that whole day; pass a full datetime (e.g. 2025-06-30T18:00:00) for a precise exclusive bound" - added
Output schema / properties / returnedAdded value: +{ + "type": "number" +}
8 tool updates
v1.1.3- Changed
export4 fields changed- added
Input schema / properties / dest / maxLengthAdded value: +4096 - added
Input schema / properties / library / maxLengthAdded value: +4096 - added
Input schema / properties / uuid / items / maxLengthAdded value: +256 - added
Input schema / properties / uuid / maxItemsAdded value: +1000
- Changed
get-photo1 field changed- added
Input schema / properties / library / maxLengthAdded value: +4096
- Changed
library-info1 field changed- added
Input schema / properties / library / maxLengthAdded value: +4096
- Changed
list-albums1 field changed- added
Input schema / properties / library / maxLengthAdded value: +4096
- Changed
list-folders1 field changed- added
Input schema / properties / library / maxLengthAdded value: +4096
- Changed
list-keywords2 fields changed- added
Input schema / properties / library / maxLengthAdded value: +4096 - added
Input schema / properties / limit / maximumAdded value: +100000
- Changed
list-persons2 fields changed- added
Input schema / properties / library / maxLengthAdded value: +4096 - added
Input schema / properties / limit / maximumAdded value: +100000
- Changed
query14 fields changed- added
Input schema / properties / album / items / maxLengthAdded value: +1024 - added
Input schema / properties / album / maxItemsAdded value: +100 - added
Input schema / properties / description / maxLengthAdded value: +2048 - added
Input schema / properties / fromDate / maxLengthAdded value: +64 - added
Input schema / properties / keyword / items / maxLengthAdded value: +1024 - added
Input schema / properties / keyword / maxItemsAdded value: +100 - added
Input schema / properties / library / maxLengthAdded value: +4096 - added
Input schema / properties / limit / maximumAdded value: +100000 - added
Input schema / properties / person / items / maxLengthAdded value: +1024 - added
Input schema / properties / person / maxItemsAdded value: +100 - added
Input schema / properties / title / maxLengthAdded value: +1024 - added
Input schema / properties / toDate / maxLengthAdded value: +64 - added
Input schema / properties / uuid / items / maxLengthAdded value: +256 - added
Input schema / properties / uuid / maxItemsAdded value: +1000
10 tool updates
v1.1.0- Added
doctor - Changed
export1 field changed- changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "exported": { + "items": { + "type": "string" + }, + "type": "array" + }, + "exportedCount": { + "type": "number" + }, + "skipped": { + "items": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "type": "array" + }, + "skippedCount": { + "type": "number" + } + }, + "type": "object" +}
- Changed
get-photo1 field changed- changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "photo": { + "additionalProperties": true, + "properties": {}, + "type": "object" + } + }, + "type": "object" +}
- Changed
health-check1 field changed- changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + }, + "ok": { + "type": "boolean" + } + }, + "type": "object" +}
- Changed
library-info1 field changed- changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "albumCount": { + "type": "number" + }, + "dbVersion": { + "type": "string" + }, + "folderCount": { + "type": "number" + }, + "keywordCount": { + "type": "number" + }, + "libraryPath": { + "type": "string" + }, + "movieCount": { + "type": "number" + }, + "personCount": { + "type": "number" + }, + "photoCount": { + "type": "number" + }, + "photosVersion": { + "type": [ + "string", + "number" + ] + }, + "totalCount": { + "type": "number" + } + }, + "type": "object" +}
- Changed
list-albums1 field changed- changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "albums": { + "items": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "type": "array" + }, + "count": { + "type": "number" + } + }, + "type": "object" +}
- Changed
list-folders1 field changed- changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "count": { + "type": "number" + }, + "folders": { + "items": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" +}
- Changed
list-keywords1 field changed- changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "count": { + "type": "number" + }, + "keywords": { + "items": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" +}
- Changed
list-persons1 field changed- changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "count": { + "type": "number" + }, + "persons": { + "items": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" +}
- Changed
query1 field changed- changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "count": { + "type": "number" + }, + "photos": { + "items": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" +}
9 tool updates
v0.1.4- First observed
export - First observed
get-photo - First observed
health-check - First observed
library-info - First observed
list-albums - First observed
list-folders - First observed
list-keywords - First observed
list-persons - First observed
query
TDQS
Every tool has a clearly distinct purpose. Overlaps like get-photo vs get-photos are resolved by single vs batch, query is for search. All operations on albums, keywords, dates, metadata are distinct and well-defined.
Most tools follow verb-noun pattern (create-album, list-albums) with hyphens. Single-word tools like 'doctor', 'query', 'export' break the pattern slightly, but overall conventions are predictable and readable.
21 tools cover the full lifecycle of managing an Apple Photos library: search, metadata, albums, keywords, dates, import/export, duplicates, and diagnostics. The count feels well-scoped without unnecessary bloat.
The tool surface is comprehensive for the domain, covering discovery, metadata editing, album management, import/export, and duplicate detection. A notable omission is the inability to delete photos (a platform limitation), but core workflows are well-supported.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Let ChatGPT, Claude & Cursor use your Mac: email, calendar, iMessage, Teams, files. Local, free.
MCP connector that lets ChatGPT list, search, and run your Apple Shortcuts via a local Mac agent
Search, read, and write your Apple Notes from ChatGPT/Claude via a local Mac agent + MCP relay.
- SomviaOAuthapp.somvia
Private Apple Health metrics and workout detail for ChatGPT, Claude, and any MCP client.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceEnables AI assistants to search, browse, and retrieve metadata and images from your Google Photos library. It supports content-based filtering, album listing, and location extraction via STDIO and HTTP transports.41-
- FlicenseNot gradedqualityDmaintenanceAllows LLMs to access, scan, and search photos on connected iOS devices via USB using libimobiledevice and ChromaDB. It supports semantic search, metadata filtering, and automated file organization for efficient media management.-
- AlicenseNot gradedqualityDmaintenanceThis MCP server enables AI tools to interact with your Apple Photos library via the osxphotos CLI, providing tools for querying and managing photos.1MIT
- AlicenseAqualityBmaintenanceEnables natural language search of local photo archives using AI-powered semantic understanding, with integration into Claude Desktop via the Model Context Protocol.42MIT
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/sweetrb/apple-photos-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server