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.

npm version npm downloads node CI OpenSSF Scorecard platform: macOS License: MIT MCP

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-thumbnail returns 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-duplicates groups 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.

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 library

Claude 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-mcp

The 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-photos

This 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 osxphotos inside that clone — a one-time, ~minute-long setup that requires Python 3.11+ on your PATH (stock macOS ships 3.9; brew install [email protected]).
  • 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-mcp

The 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)

Configuration for two more hosts is included — each registers 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.

Manual Installation

1. Install the server:

npm install -g apple-photos-mcp

2. 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 instant

Auto-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 [email protected]). Pre-warm it with pnpm run setup if 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 (addedInLast: "7d" for "recently imported"), album, keyword, person, ML label, place name, GPS radius (near), folder, year, file size, media type (screenshot, screen recording, selfie, panorama, live, portrait, time-lapse, slow-mo, burst), aesthetic score (minScore), OCR-detected text (detectedText), favorite/hidden flags, or title/description substring — with newestFirst ordering
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 score, OCR detectedText), shared-album social data (owner, comments, likes), optional burst-sibling expansion, and type flags (HDR, live, portrait, panorama, raw, edited, etc.)
Batch Details get-photos fetches full metadata for up to 50 UUIDs in one call
Selection Bridge get-selected-photos returns the photos currently selected in the Photos.app window — "act on these photos"
Thumbnails get-thumbnail returns a photo as an inline viewable image (MCP image content block) from Photos' pre-generated derivatives — see photos without exporting
Find Duplicates find-duplicates groups exact duplicates using Photos' own fingerprint detection
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 create-album creates an album (optionally nested in a folder path); idempotent — an existing album of that name is returned instead of duplicated
Add to Album add-to-album files photos (by UUID) into an album; idempotent, reports added / already-present / not-found per UUID
Remove from Album remove-from-album takes photos out of an album — never out of the library (see the caveats)
Set Metadata set-photo-metadata sets title / description / favorite, echoing before/after values so changes can be reverted
Set Keywords set-keywords adds/removes keywords with union semantics — existing keywords you don't mention are always preserved
Set Date set-photo-date fixes a photo's date/time (absolute or shifted by seconds) — dry run by default, with before/after echoed for reverts; Photos-library date only, EXIF untouched
Import import-photos brings files into the library (optionally into an existing album) — add-only, with source paths validated against the same allowlist as export

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-album changes 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 for import-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 notFound lists; 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-keywords uses union semantics (read-merge-write) so keywords you don't mention are never clobbered; album adds are idempotent; set-photo-date is a dry run by default — it writes nothing until you pass dryRun: 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 library parameter 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-album rebuilds 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: the resolved Python interpreter (path + version — warns when it's older than the required 3.11, with brew install [email protected] advice), osxphotos installation, 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
library string No Path to a non-default .photoslibrary (defaults to system library)

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
uuid string[] No Specific UUIDs to fetch (max 1000 entries, each ≤ 256 chars)
album string[] No Album name(s); ANY-match, exact full folder path + name (max 100 entries)
keyword string[] No Keyword(s); ANY-match, exact whole-string (max 100 entries)
person string[] No Person name(s); ANY-match, exact whole-string (max 100 entries)
fromDate string No ISO 8601 inclusive lower bound on photo date (e.g. "2025-06-01")
toDate string No ISO 8601 upper bound on photo date. A bare date (e.g. "2025-06-30") includes that whole day; a full datetime (e.g. "2025-06-30T18:00:00") is a precise exclusive bound
favorite boolean No Only favorites
notFavorite boolean No Exclude favorites
hidden boolean No Only hidden photos
notHidden boolean No Exclude hidden photos (default behavior)
photos boolean No Include still photos
movies boolean No Include movies
title string No Substring match on title (case-sensitive, ≤ 1024 chars)
description string No Substring match on description (case-sensitive, ≤ 2048 chars)
addedAfter string No ISO 8601 inclusive lower bound on IMPORT date (dateAdded — when the photo entered the library, not when it was taken)
addedBefore string No ISO 8601 upper bound on import date; a bare date includes that whole day
addedInLast string No Imported within a trailing window — "<number><unit>", unit s/m/h/d/w (e.g. "7d", "24h")
label string[] No ML classification label(s) Photos computed (the labels field of get-photo, e.g. Dog, Beach); ANY-match, exact whole-string (max 100)
folder string[] No Folder name(s)/path(s) — photos in albums inside the folder; ANY-match (max 100)
place string[] No Place-name substring(s) from reverse geocoding (city, region, landmark). Multiple values are ANDed, not ORed (max 100)
hasLocation boolean No true = only photos WITH GPS coordinates; false = only photos WITHOUT; omit for no filter
near string No GPS-radius filter: "lat,lon,radiusKm" (e.g. "46.5,-87.4,5") — only photos within the great-circle radius of the point. Composes (AND) with every other filter; photos without GPS data never match
year number[] No Taken in calendar year(s); ANY-match (max 100)
minSize number No Original file size at least this many bytes
maxSize number No Original file size at most this many bytes
noKeyword boolean No Only photos carrying no keyword at all
burst boolean No Only burst photos
screenshot / screenRecording / selfie / panorama / live / portrait / timelapse / slowMo boolean No Media-type filters — each true narrows to only that type
video boolean No Only videos/movies (alias of movies)
minScore number 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; photos without a computed score never match
detectedText 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
newestFirst boolean No Sort by taken date, newest first, before limit is applied — so limit means "the N most recent matches"
limit number No Cap the number of results returned (default 500 when omitted, max 100000)
library string No Path to a non-default .photoslibrary

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
uuid string Yes Photo UUID, as returned by query (hexadecimal segments separated by dashes, max 256 chars — anything else is rejected before the library is even opened)
burstPhotos boolean No true = also return burstPhotos: the other frames of this photo's burst set (UUID, filename, date each; empty when the photo isn't a burst member)
library string No Path to a non-default .photoslibrary

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
uuid string[] Yes 1–50 photo UUIDs, as returned by query (same hex-with-dashes format as get-photo)
library string No Path to a non-default .photoslibrary

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 library parameter.

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
uuid string Yes Photo UUID, as returned by query
minSize number No Smallest acceptable long-edge size in pixels (default 360, max 8192). The smallest qualifying derivative is served — raise it (e.g. 1024) when you need detail like small text
library string No Path to a non-default .photoslibrary

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).


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
limit number No Max duplicate groups to return (default 100; groupCount reports the total)
library string No Path to a non-default .photoslibrary

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, collect them into a quarantine album in Photos.app and review/delete there.


Browse

list-albums

List all albums in the library.

Parameter Type Required Description
library string No Path to a non-default .photoslibrary

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
library string No Path to a non-default .photoslibrary

Returns: Each folder's title, parent folder, album count, and subfolder count.


list-keywords

List keywords sorted by usage count.

Parameter Type Required Description
limit number No Cap to top-N keywords
library string No Path to a non-default .photoslibrary

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
limit number No Cap to top-N persons
library string No Path to a non-default .photoslibrary

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
uuid string[] Yes Photo UUID(s) to export (1–1000 entries, each ≤ 256 chars)
dest string Yes Destination directory (created if missing). Must resolve — after expanding ~ and following symlinks — to a path under your home directory, /tmp, /private/tmp, or /Volumes; anything else is rejected
edited boolean No Export the edited version instead of the original
live boolean No Also export the live-photo video
raw boolean No Also export the raw image
overwrite boolean No Overwrite existing files at the destination. Without it, a photo whose file already exists is skipped (reported per-UUID) — never duplicated
library string No Path to a non-default .photoslibrary

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 five 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
name string Yes Album name (≤ 255 chars)
folder string No Folder path to nest the album under, /-separated for nesting (e.g. "Trips/2026"); folders are created as needed. (Folder names containing a literal / are not addressable.)

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
album string Yes Album name or UUID (UUID-looking values try the id lookup first, then fall back to a name match)
uuid 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
album string Yes Album name or UUID
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).

set-photo-metadata

Set a photo's title, description, and/or favorite flag. Only the fields you pass are touched.

Parameter Type Required Description
uuid string Yes Photo UUID
title string No New title (empty string clears it)
description string No New description (empty string clears it)
favorite 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
uuid string Yes Photo UUID
add string[] No Keywords to add (≤ 100; created in Photos if new)
remove string[] No Keywords to remove from this photo (≤ 100; 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
uuid string Yes Photo UUID
date string * 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
shiftSeconds number * Shift the curre