Skip to main content
Glama
sweetrb

apple-photos-mcp

by sweetrb

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.

Related MCP server: mcp-osxphotos

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

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

    Or add it to ~/.hermes/config.yaml by 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.json to ~/.gemini/config/mcp_config.json (or via Antigravity's MCP settings).

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 python@3.12). 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 — 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

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

limit

number

No

Max duplicate groups to return (default 100, max 10000; 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, 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

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 (max 100000)

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 (max 100000)

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

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

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

uuid

string

Yes

Photo UUID

title

string

No

New title (≤ 255 chars; empty string clears it)

description

string

No

New description (≤ 2048 chars; 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 entries, each ≤ 255 chars; created in Photos if new)

remove

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

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 current date by this many seconds (negative = earlier; -86400 = one day back)

dryRun

boolean

No

Default true — preview the before/after dates without writing. Pass false to apply

* 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

paths

string[]

Yes

1–50 absolute (or ~-prefixed) file paths. Each must exist, under your home directory, /tmp, /private/tmp, or /Volumes

album

string

No

Existing album (name or UUID) to file the imports into — create it with create-album first; a missing album is an error, not auto-created

skipDuplicateCheck

boolean

No

Default false: Photos checks for duplicates, and a found duplicate raises a blocking dialog in Photos.app that a human must answer. true skips the check — duplicates WILL be re-imported silently

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 install -g apple-photos-mcp

osxphotos 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 build

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

  1. pnpm run build — compile the TypeScript to build/index.js.

  2. pnpm run setup — optional; pre-builds the project-local venv at ./venv with osxphotos (the server prefers ./venv/bin/python3). Skip it and the server builds the venv on the first tool call.

  3. 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_ROOT is 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 use CLAUDE_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.json is dedicated to the clone workflow (using ${CLAUDE_PROJECT_DIR:-.}). Because plugin.json declares its own mcpServers, 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 an apple-photos entry 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.json is 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

  1. Open System Settings (or System Preferences on older macOS)

  2. Go to Privacy & Security > Full Disk Access

  3. Click the + button

  4. Add the application that hosts the MCP server:

    • Claude Desktop: Add /Applications/Claude.app

    • Terminal: Add /Applications/Utilities/Terminal.app

    • VS Code: Add /Applications/Visual Studio Code.app

    • iTerm: Add /Applications/iTerm.app

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

APPLE_PHOTOS_MCP_ENABLE_WRITES

unset (read-only)

Set to 1 to enable the write tools (create-album, add-to-album, remove-from-album, set-photo-metadata, set-keywords, set-photo-date, import-photos). Until then every write tool returns a clear opt-in error and the server cannot modify the library. Restart the server after changing it.

APPLE_PHOTOS_MCP_MAX_BUFFER

104857600 (100 MB)

Max bytes captured from the Python sidecar's stdout. Raise it if a very large library/query is truncated; lower it to cap memory.

APPLE_PHOTOS_MCP_TIMEOUT

60000 (60 s)

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 query, get-photo, get-photos, get-thumbnail, library-info, list-albums, list-folders, list-keywords, list-persons and health-check, and is ignored by every tool that carries a fixed budget: get-selected-photos 2 min; find-duplicates, create-album, add-to-album, set-photo-metadata, set-keywords and set-photo-date 5 min; remove-from-album and import-photos 10 min; export 30 min. Those budgets are not configurable — a timeout on one of them says so instead of pointing here.

APPLE_PHOTOS_MCP_STALE_WHILE_REVALIDATE

unset (off)

Set to 1 to serve the previously parsed library immediately when Photos.sqlite has changed, and re-parse on a background thread instead of blocking the request. The persistent sidecar re-validates its parsed PhotosDB against Photos.sqlite's mtime on every call, so any write to the library — a phone sync, an edit, Photos.app housekeeping — makes the next request pay a full re-parse; on a 15 GB library that is minutes. With this set, that request answers from the last good parse and the refresh lands behind it. The trade is freshness: a result may predate an import by one refresh cycle, and responses carry dbStale: true while that is the case. Peak memory roughly doubles during a refresh, since both parses are briefly resident. Worth setting only on libraries big enough for the parse to dominate.

APPLE_PHOTOS_MCP_PERSISTENT_SIDECAR

unset (persistent mode on)

Set to 0 (or false) to disable the long-lived serve-mode sidecar and spawn a fresh Python process per call (pre-1.4.0 behavior). Every call then re-pays the full library parse — only useful for debugging.

APPLE_PHOTOS_MCP_SIDECAR_IDLE_MS

300000 (5 min)

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. 0 = never kill on idle.

APPLE_PHOTOS_MCP_NO_AUTO_SETUP

unset (auto-setup on)

Set to 1 (or any truthy value) to disable the automatic first-use venv bootstrap. With it on, you must run pnpm run setup (or pip3 install osxphotos) yourself.

APPLE_PHOTOS_MCP_SETUP_TIMEOUT

300000 (5 min)

Max time, in milliseconds, the automatic venv bootstrap may run before it's aborted. Raise it on slow networks where the osxphotos install needs longer. Also bounds how long a second server instance waits on the cross-process setup lock for a concurrent bootstrap to finish (simultaneous first calls can't corrupt the venv).

APPLE_PHOTOS_MCP_CONFIG_FILE

~/Library/Application Support/apple-photos-mcp/config.json

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 inputSchema and outputSchema is 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 outgoing tools/list payload is normalized at the transport boundary (src/utils/jsonSchemaDialect.ts).

  • A bundled Python script (src/utils/photos_reader.py) uses osxphotos to 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 long query or a minutes-long iCloud export.

  • 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's sidecar_mode check 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 — export writes 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 (APPLE_PHOTOS_MCP_ENABLE_WRITES=1) and limited to albums, metadata (incl. dates), and add-only imports

No photo deletion

Deliberate: no tool deletes photos, albums, or folders — quarantine into an album and delete in Photos.app

remove-from-album rebuilds the album

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 library parameter applies to read tools only

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 ./venv and installing osxphotos). It can take ~a minute and logs progress to stderr. Subsequent calls are fast. Pre-warm with pnpm run setup to 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 python3 is 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 pip or network access is unavailable, or when you set APPLE_PHOTOS_MCP_NO_AUTO_SETUP=1. Fix the missing piece (or unset the variable) and retry — or install by hand with pip3 install osxphotos (global) or scripts/setup.sh / pnpm run setup from a repo checkout (project-local venv).

  • Run the doctor tool 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 query to get current UUIDs.

  • The photo may have been permanently deleted from the library.

  • export says "UUID not found (deleted or in trash)" but get-photo returns the photo? The photo is in Recently Deleted: get-photo falls back to the trash, while query and export read 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=true was 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=true was 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=1 in 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 doctor tool to confirm the gate state (its writes check 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, or pnpm run build from 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 claude from inside the repo directory so CLAUDE_PROJECT_DIR resolves to the repo root. The bare . fallback resolves against the launching process's working directory, not the repo, and is unreliable.

  • Run pnpm run build first — the entrypoint ${CLAUDE_PROJECT_DIR:-.}/build/index.js won't exist until you compile.

  • The ./venv with osxphotos builds automatically on the first tool call; run pnpm run setup only to pre-warm it, or if you've set APPLE_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 list and check for conflicting scopes. Project-scope (.mcp.json) outranks user-scope; a stale user-scope apple-photos entry 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 code

The 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/out

Author

Rob Sweet - President, Superior Technologies Research

A software consulting, contracting, and development company.

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.

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 tools
add-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
uuidYesPhoto UUID(s) to add (1–100, as returned by query)
albumYesAlbum name or UUID (UUID-looking values try the id lookup first)

Output Schema

ParametersJSON Schema
NameRequiredDescription
addedNo
albumNo
notFoundNo
addedCountNo
alreadyPresentNo

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations exist, so the description carries the full burden — and it delivers thoroughly. It discloses write semantics with an environment-var gate, idempotency, non-destructiveness (photos never copied/modified/deleted), the 100-UUID cap, AppleScript driving, Photos.app launching, and the Automation permission requirement.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but uses labeled, front-loaded sections ('Use when', 'Returns', 'Do not use when', 'Safety') that make scanning easy. Every sentence carries information; the length is justified by the complexity of a write tool with environment gating, permissions, and return semantics.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a write tool with zero annotations, the description is remarkably complete: it covers return shape, failure modes, prerequisites, environmental gating, side effects (launching Photos.app), permission requirements, and the library-targeting caveat. No critical information an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

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 value beyond the schema: it explains the album parameter's lookup behavior ('UUID-looking values try the id lookup first'), maps return fields (addedCount, alreadyPresent, notFound) back to parameter outcomes, and clarifies failure conditions tied to the inputs.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource ('file photo UUIDs into an album') with concrete example use cases, and differentiates from siblings by naming create-album and remove-from-album. An agent can immediately tell what this tool does and 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.

Usage Guidelines5/5

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 give direct conditions and name the alternative tools (create-album for missing albums, remove-from-album for removing photos). The prerequisite about album existence is stated explicitly.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYesAlbum name
folderNoFolder path to nest the album under, "/"-separated for nesting (e.g. "Trips/2026"); folders are created as needed

Output Schema

ParametersJSON Schema
NameRequiredDescription
albumNo
createdNo

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. It explicitly states this is a WRITE tool, requires APPLE_PHOTOS_MCP_ENABLE_WRITES=1, launches Photos.app, requires macOS Automation permission, and writes to the currently open library. It also clarifies that it only creates albums/folders and never deletes/moves/modifies photos. This goes well beyond what structured annotations would provide and fully discloses side effects and prerequisites.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with clear labeled sections: 'Use when', 'Returns', 'Do not use when', and 'Safety'. It front-loads the primary purpose and conditions, then covers return semantics and safety. Every sentence serves a purpose: providing usage context, return behavior, alternatives, or safety/prerequisites. Despite length, it is efficient and easy to parse for an agent.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with side effects, permissions, and environmental dependencies, the description is exceptionally complete. It covers purpose, usage scenarios, return value semantics, idempotency, safety restrictions, environment variable requirement, automation permission, and library targeting. The output schema is present (as per context signals), so return format is already documented. Nothing critical is missing for an agent to call this tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Although schema coverage is 100% (both name and folder have descriptions), the description adds meaningful context: it explains folder path syntax (e.g., 'Trips/2026'), that folders are created as needed, and the idempotent matching behavior ('without folder the name is matched anywhere in the library, with folder only inside that folder'). It also explains the 'created' return flag and its semantics. This adds value beyond the schema's basic parameter descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a precise statement: 'you need an album to file photos into — a new album by name, optionally nested inside a folder path'. It specifies the verb (create), resource (album), and gives concrete examples like quarantine album or per-trip album. It also differentiates from siblings by explicitly naming list-albums and add-to-album as alternatives, so an agent can clearly distinguish this tool from related ones.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description includes a 'Use when' section that provides concrete scenarios and a 'Do not use when' section that names the correct sibling tools (list-albums for listing, add-to-album for adding photos). It also explains idempotency and the behavior when the album already exists, which helps the agent decide when to call this tool versus alternatives. This is explicit and thorough.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
checksNo
healthyNo

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden. It describes what the tool checks and the ok/warn/fail format, and implies a non-destructive diagnostic role through 'before querying, exporting, or writing'. However, it does not explicitly state that it performs no modifications, a minor gap given the diagnostic context. Overall transparent about its operation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is structured with clear 'Use when', 'Returns', and 'Do not use when' sections, and each sentence contributes valuable information. It is somewhat lengthy for a no-arg tool, but the detail is justified given the diagnostic scope and the need to differentiate from siblings. Slightly long prevents a 5.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description fully covers the tool's purpose, use cases, return content (six checks, each with ok/warn/fail and actionable advice), and alternatives. With no parameters and an existing output schema, nothing an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters and an empty input schema, so the description need not explain parameter semantics. Baseline of 4 is appropriate; the description adds context about what the tool does with no parameters, which is sufficient.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool performs a 'full setup diagnostic' and enumerates the six specific checks (Python interpreter, osxphotos install, sidecar mode, write-tools gate, Photos library, Full Disk Access). It distinguishes itself from the sibling 'health-check' by specifying the scenario (when a permission/error occurs or before querying/exporting/writing) vs the lightweight smoke test. No ambiguity exists about what it does.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit 'Use when' and 'Do not use when' clauses define the precise conditions for invocation, and explicitly name the alternative tool ('health-check') for the opposite case. This leaves no inference burden on the agent.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
rawNoAlso export the raw image
destYesDestination directory (created if missing). Must be under the home directory, /tmp, /private/tmp, or /Volumes
liveNoAlso export the live-photo video
uuidYesPhoto UUID(s) to export
editedNoExport the edited version instead of the original
libraryNoPath to a .photoslibrary (default: system Photos library)
overwriteNoOverwrite existing files at the destination

Output Schema

ParametersJSON Schema
NameRequiredDescription
skippedNo
exportedNo
destinationNo
skippedCountNo
exportedCountNo

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure, and it does so thoroughly: it reveals file writes, directory creation, overwrite semantics, skip behavior, iCloud fallback via AppleScript, performance implications, and return shape. This is exemplary transparency for a side-effecting tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but every sentence earns its place. It is structured with clear labels ('Use when', 'Do not use when', 'Returns', 'Safety') and front-loads the primary use case before diving into edge cases. This is appropriate density for a high-complexity tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 7-parameter, side-effecting tool with no annotations, the description covers all needed operational context: parameters, destination constraints, overwrite behavior, skip reasons, return values, progress notifications, iCloud fallback, and permissions. Nothing an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

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 behavior beyond the schema: it explains that edited/live/raw modify the default original export, that overwrite=true overwrites in place, and that dest must resolve to an allowed root. Only the library parameter is not elaborated, but its schema description is already sufficient.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action: copying one or more photos by UUID to a destination directory on disk. It distinguishes itself from siblings by explicitly routing metadata-only needs to get-photo and exploration needs to query, so an agent can pick the right tool without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description includes explicit 'Use when' and 'Do not use when' guidance, names the exact alternatives (get-photo, query), and covers edge conditions like iCloud downloads requiring Photos.app and Automation permission. This leaves little to inference about when the tool should be invoked.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax duplicate groups to return (default 100; groupCount reports the total)
libraryNoPath to a .photoslibrary (default: system Photos library)

Output Schema

ParametersJSON Schema
NameRequiredDescription
groupsNo
returnedNo
groupCountNo

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations are not provided, so the description carries the full burden. It clearly states the tool is read-only and cannot delete photos, which is critical for an AI agent to avoid destructive actions. It also explains the default cap of 100 groups and the return ordering, but does not mention external rate limits or auth requirements. The read-only disclosure is a key extra 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.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured: it opens with a clear 'Use when' and 'Returns' section, then moves to 'Do not use when' and 'Safety'. Each section is purposeful and uses bullet-like formatting for readability. It front-loads the key use case and return info, and every sentence contributes to guiding the agent.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's moderate complexity (two optional parameters, output schema provided) and the absence of annotations, the description is complete. It covers the tool's purpose, usage, safety, return format, and parameter defaults, and even suggests follow-up tools (get-thumbnail, create-album, add-to-album) for acting on results. Nothing essential is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

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 value by explaining the default behavior of the limit (default 100, groupCount reports total) and that the library parameter is optional (defaults to system library). It also clarifies that groups are returned newest-first, which is not in the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

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 specific use cases like cleaning up after double imports and auditing before migration. It distinguishes from near-duplicate detection by noting that only exact matches are found, setting it apart from sibling tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states when to use (finding exact duplicates) and when not to use (near-duplicates, similar shots), and explains the limitation of fingerprint matching. Also provides actionable alternative steps (quarantine into album) for handling duplicates, which guides the agent toward the correct workflow.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
uuidYesPhoto UUID (hex-with-dashes, as returned by query)
libraryNoPath to a .photoslibrary (default: system Photos library)
burstPhotosNotrue = include burstPhotos: the OTHER frames of this photo's burst set (empty when the photo is not a burst member)

Output Schema

ParametersJSON Schema
NameRequiredDescription
photoNo

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the behavioral burden. It thoroughly discloses conditional output behavior: null EXIF fields, macOS version differences, iCloud shared-album data only for shared assets, and burstPhotos semantics. It does not explicitly state read-only/no side effects, but 'get' and 'Returns' make the read-only nature strongly implied.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but well-structured: use-case first, then return content, then exclusions. The metadata enumeration is dense but provides real selection and invocation value. Every section earns its place, though some return-field detail could be trimmed.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's 3 parameters, existing output schema, and lack of annotations, the description is complete: it explains when to use it, what it returns, how optional parameters behave, and which sibling tools to use instead. Nothing significant is missing for an agent to invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3. The description adds useful context about uuid provenance and burstPhotos behavior, but the schema already documents all three parameters accurately. No additional parameter meaning is strictly required.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: getting a single photo's complete metadata by UUID. It explicitly distinguishes itself from get-photos and get-thumbnail, so an agent immediately understands what this tool uniquely does.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

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' guidance with named alternatives: query, get-photos, and get-thumbnail. The conditions are concrete, so the agent knows exactly when to choose this tool over its siblings.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
uuidYesPhoto UUIDs (1–50, as returned by query)
libraryNoPath to a .photoslibrary (default: system Photos library)

Output Schema

ParametersJSON Schema
NameRequiredDescription
countNo
photosNo
notFoundNo

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the transparency burden. It discloses the batch cap, the single sidecar round-trip vs N calls, the return shape including notFound for unmatched UUIDs, and the included metadata blocks. It does not explicitly state read-only/non-mutating behavior, but the 'get' verb and output description make that reasonably clear.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured and front-loaded: it opens with the use case, then summarizes the return value, then lists exclusions. Every sentence carries routing or behavioral information, and there is no filler or tautology.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a two-parameter batched getter, the description is self-sufficient: it explains when to use it, what is returned, how missing UUIDs are surfaced via notFound, and which sibling tools to use instead. The output schema covers detailed field structure, so nothing essential is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3. The description adds helpful source context ('typically from query or find-duplicates') and reinforces the 50-UUID cap, but it does not add substantial meaning beyond the schema's uuid and library descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a precise purpose: retrieve full metadata for multiple photo UUIDs in one batched call, with the same per-photo shape as get-photo. It also names the typical input sources (query or find-duplicates), making the tool's role clear and distinct from siblings like get-photo and get-thumbnail.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit 'Use when' conditions (several UUIDs for dedupe review, EXIF audit, captioning pass) and explicit 'Do not use when' conditions with named alternatives: get-photo for a single UUID, query when no UUIDs exist, and get-thumbnail for viewing images. This is model routing guidance, not just a vague hint.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
countNo
photosNo
notFoundNo

TDQS

A4.6/5.0
Behavior4/5

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 that the tool is read-only, that it reads via AppleScript, that it requires Photos.app running with a visible selection, that it requires macOS Automation permission with a one-time system prompt, and that it returns notFound entries for uncheckpointed imports. It also notes the tool never launches Photos itself. This is rich behavioral context, though it could have added details about error behavior or side effects beyond the one-time permission prompt.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with clear sections: 'Use when', 'Returns', 'Do not use when', and a note. It front-loads the trigger phrase and the core purpose. It is slightly verbose in the note about permissions and notFound, but every sentence earns its place by conveying operational prerequisites or output details. A minor deduction for the note being a bit dense.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter tool with no annotations, the description covers the input source, output shape, error conditions, prerequisites, permission requirements, and relationship to sibling tools. The output schema exists and the description summarizes its key fields without needing to enumerate them. Nothing an agent needs to decide whether to call this tool and interpret its results is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

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 complete. The description adds meaning by explaining what the tool consumes implicitly (the GUI selection) and what it returns (count, photo summaries, notFound). With 0 params, the baseline is 4, and the description fully compensates for the lack of parameters by clarifying the implicit input source.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: it retrieves the identities of photos the user has selected in the Photos.app GUI. It clearly distinguishes itself from siblings by naming query, get-photos, get-thumbnail, export, and add-to-album as downstream consumers of its output, and by explicitly saying 'Do not use when' you want to FIND photos by criteria — use query. This is a precise, non-tautological definition.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit when-to-use triggers ('these photos' / 'the selected photos'), explicit when-not-to-use conditions (finding by criteria → query; Photos.app not running / nothing selected → clear error), and names the alternative tool. It also states prerequisites (Photos.app running with a visible selection, macOS Automation permission). This is exemplary usage guidance.

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

ParametersJSON Schema
NameRequiredDescriptionDefault
uuidYesPhoto UUID (hex-with-dashes, as returned by query)
libraryNoPath to a .photoslibrary (default: system Photos library)
minSizeNoSmallest acceptable long-edge size in pixels (default 360). The smallest qualifying derivative is served, so higher values return larger images

Output Schema

ParametersJSON Schema
NameRequiredDescription
pathNo
uuidNo
widthNo
heightNo
byteSizeNo
mimeTypeNo
isDerivativeNo

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, description carries full burden. It discloses return format (inline MCP image), derivative selection logic, isDerivative flag, downscaling via sips, and limitations (iCloud). All behavioral traits 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.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with 'Use when' and 'Returns', then exclusions. Though longer than average, every sentence adds necessary context for a complex tool. No redundancy; well organized.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Comprehensive for the tool's complexity: covers output schema mentions, edge cases, alternatives, and parameter semantics. An agent has all information to select and call correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema covers 100% of parameters, but description adds value: explains minSize semantics (smallest qualifying derivative, default 360, raise for detail), and isDerivative interpretation. This goes beyond schema descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description states a specific verb+resource (get thumbnail to SEE a photo) and distinguishes from siblings: 'Prefer this over export' and 'use get-photo' for metadata. It clearly differentiates the tool's role.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when to use ('want to SEE a photo'), when not to ('need full-resolution file — use export; only metadata — use get-photo'), and edge cases (movies, iCloud-only) are covered. No ambiguity remains.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
okNo
messageNo

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full behavioral burden. It discloses the return payload (ok/fail, version, path, photo count) and the liveness behavior when another operation is running. This is valuable context. It doesn't mention permission requirements or error specifics, but those are handled by the doctor tool, so this is acceptable. A small gap is the lack of explicit statement that it is read-only, but the smoke-test nature implies that.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is efficiently structured with clear sections (Use when, Returns, Do not use when). It is front-loaded with the purpose and avoids fluff. Every sentence adds useful information without redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter health-check tool, the description covers its purpose, return values, and behavioral nuance (liveness) while distinguishing from the more thorough doctor tool. The existence of an output schema covers the detailed return format. Nothing critical is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, so the description has no parameter meanings to explain. The baseline for 0 params is 4, and the description correctly omits parameter details. No additional value needed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states a specific purpose: a quick smoke test for osxphotos installation and library openability. It also explicitly distinguishes itself from the 'doctor' tool, which is a fuller diagnostic. This makes the tool's role unambiguous and separates it from siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

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' conditions, naming the alternative (doctor) and the specific scenario where that alternative should be chosen. This gives an agent clear routing guidance.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
albumNoEXISTING album (name or UUID) to file the imports into — create it with create-album first if needed
pathsYesAbsolute (or ~-prefixed) file paths of images/videos to import (1–50). Must exist, under your home directory, /tmp, /private/tmp, or /Volumes
skipDuplicateCheckNotrue = 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

ParametersJSON Schema
NameRequiredDescription
albumNo
importedNo
importedCountNo
requestedCountNo

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full behavioral burden and handles it thoroughly. It discloses the write-safety env var requirement, that imports only add and never modify/delete, that source files remain untouched, that imports cannot be undone programmatically, that duplicate checks can trigger blocking dialogs, and that AppleScript/Automation permissions are required.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long, but each section earns its place for a write tool with significant safety caveats. It is structured by purpose, return values, exclusions, and safety, with critical constraints front-loaded in the 'Use when' and 'Do not use when' sections.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity, zero annotations, and write-side effects, the description covers everything an agent needs: when to use it, what it returns, path validation, duplication behavior, permission requirements, error conditions, and undo limitations. The output schema exists, and the description still explains the meaning of the return counts and IDs.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all three parameters well. The description reinforces key constraints (existing album, allowed path roots, duplicate-check behavior), but it does not add substantial new semantics beyond what the schema's property descriptions already provide.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool imports image/video files from disk into the Photos library, and distinguishes it from siblings by describing the return contract and what it does not do. The 'Use when' examples (round-trip edits, folder scans, SD-card ingest) make the purpose concrete and identifiable.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly says when to use the tool and when not to, including the prerequisite that the target album must already exist and that create-album should be called first. It also gives path restrictions and duplicate-handling guidance, making the selection and invocation decision fully explicit.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
libraryNoPath to a .photoslibrary (default: system Photos library)

Output Schema

ParametersJSON Schema
NameRequiredDescription
dbVersionNo
albumCountNo
movieCountNo
photoCountNo
totalCountNo
folderCountNo
libraryPathNo
personCountNo
keywordCountNo
photosVersionNo

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It states exactly what the tool returns (library path, versions, and six counts) and implies it is a read-only operation. It does not disclose any potential side effects or error conditions, but for an info-gathering tool this is acceptable. The description is clear and consistent, with no contradictions.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with clear sections for usage and returns. It is slightly verbose in the 'Do not use' portion, but every sentence serves a purpose. The key purpose is front-loaded, making it easy for an agent to quickly grasp the tool's role.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (one optional parameter) and the presence of an output schema, the description covers all necessary aspects: purpose, return values, and usage guidance. It does not discuss error handling or performance, but these are not critical for a basic info tool. The description is sufficiently complete for an agent to invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% for the single optional parameter (library), and the schema description already includes the default behavior. The tool description does not add new semantic meaning beyond what the schema provides, so the baseline score of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description explicitly states the tool's purpose: to provide high-level stats about the whole library (total counts of photos, movies, albums, folders, keywords, persons) and to confirm the target library. It clearly distinguishes itself from sibling tools by naming alternatives (list-albums, query) and 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.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description includes explicit 'Use when' and 'Do not use when' sections, listing concrete conditions and naming the specific alternative tools for each exclusion case. This leaves no ambiguity about when to invoke this tool versus others.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
libraryNoPath to a .photoslibrary (default: system Photos library)

Output Schema

ParametersJSON Schema
NameRequiredDescription
countNo
albumsNo

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the behavioral disclosure burden. It clearly states the operation returns every album's key fields and implies a read-only listing, but it does not mention edge cases such as empty libraries, permission issues, or default library behavior. Still, the disclosed behavior is sufficiently clear for this tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is tightly structured with 'Use when', 'Returns', and 'Do not use when' sections, making it scannable and front-loaded. Every sentence contributes actionable information without redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers what the tool returns, when to use it, when not to use it, and which sibling to choose instead. It also has an output schema, so return-value details are further grounded. Nothing critical is missing for an agent to select and invoke this tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the single optional library parameter is already documented in the schema. The tool description does not add additional parameter semantics, but none are necessary given the schema's clarity.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: it lists the catalog of albums, and specifies exactly what is returned (title, folder path, photo count, shared status, UUID). It also differentiates itself from related tools like list-folders and query by explicitly naming what it is not.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The 'Use when' section gives concrete scenarios, and the 'Do not use when' section names specific alternatives with reasons, such as using query for photos inside an album and library-info for total album count. This gives an agent unambiguous routing guidance.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
libraryNoPath to a .photoslibrary (default: system Photos library)

Output Schema

ParametersJSON Schema
NameRequiredDescription
countNo
foldersNo

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the behavioral burden. It clearly indicates the scope of the operation ('every folder's title, parent folder, album count, and subfolder count') and implies a non-mutating read operation via 'list' and 'Returns.' It does not discuss edge cases like hidden folders or ordering, but it still provides meaningful 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.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is tightly organized into three front-loaded sections: use-when, returns, and do-not-use-when. Every sentence earns its place, and there is no filler or repetition of schema information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

This is a low-complexity tool with one optional parameter, a complete input schema, and an output schema. The description covers purpose, usage boundaries, alternatives, and return scope, making it adequately complete for an agent to select and invoke the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, and the sole optional 'library' parameter is already documented in the schema as 'Path to a .photoslibrary (default: system Photos library).' The description adds no additional parameter-level meaning, so the baseline of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names a specific verb and resource ('list-folders') and defines folders as 'the containers that hold albums and subfolders.' It clearly differentiates this from sibling tools by saying 'use list-albums' for albums and 'use library-info' for a total folder count.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

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' guidance, naming the exact alternative tools (list-albums, library-info) and the conditions that select them. This leaves no ambiguity about when to invoke the tool versus its siblings.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoTop-N keywords
libraryNoPath to a .photoslibrary (default: system Photos library)

Output Schema

ParametersJSON Schema
NameRequiredDescription
countNo
keywordsNo

TDQS

A4.5/5.0
Behavior4/5

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, sorted most-used first) and the effect of the limit parameter. While it doesn't explicitly state it is read-only, the list operation implies no side effects, which is adequate given the tool's nature.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with clear sections: 'Use when', 'Returns', and 'Do not use when'. It is concise, front-loaded with the primary use case, and every sentence adds value without redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool has only two optional parameters, both well-documented in the schema. The description provides usage context, exclusions, and return format. Since an output schema exists, the description need not detail the return structure further. It is fully sufficient for an agent to correctly invoke the tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage for both parameters. The description adds minor context for limit ('top-N') but does not go beyond the schema's own description. Since the schema already documents the parameters well, the description adds limited value, warranting the baseline score of 3.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool lists the catalog of keywords/tags in the library, with a specific verb and resource. It distinguishes itself from siblings like list-persons and query by explicitly naming what it does not do, making its purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

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 alternatives (query for photos by keyword, list-persons for people/faces). This directly tells an agent when to select this tool versus its siblings.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoTop-N persons
libraryNoPath to a .photoslibrary (default: system Photos library)

Output Schema

ParametersJSON Schema
NameRequiredDescription
countNo
personsNo

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the behavioral burden. It discloses the output ordering, the inclusion of photo counts, and the _UNKNOWN_ representation for unidentified faces. It does not explicitly state that the operation is read-only, but the listing semantics make that relatively clear.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact, well-structured, and front-loaded with usage intent. Every sentence serves a purpose: when to use, what it returns, and when not to use it.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple read-only listing tool with only two optional parameters and an output schema, the description covers the essential behavior, return shape, edge case for unknown faces, and sibling routing. Nothing critical is missing for an agent to call this tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents both limit and library. The description reinforces that limit selects the top-N persons, but it does not add meaningful semantic detail 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.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states that the tool returns a catalog of named people from Photos face recognition, including photo counts, sorted most-photographed first. It also names the specific sibling tools it is not (query and list-keywords), so an agent can disambiguate immediately.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

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 concrete alternatives: use query for photos of a person, and list-keywords for subject tags. This leaves no ambiguity about when to invoke the tool.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
liveNoOnly live photos
nearNoGPS-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
uuidNoSpecific UUIDs to fetch
yearNoTaken in calendar year(s); ANY-match (e.g. [2024, 2025])
albumNoAlbum name(s); ANY-match
burstNoOnly burst photos
labelNoML classification label(s) from Photos object detection (the labels field of get-photo, e.g. Dog, Beach, Text); ANY-match, exact whole-string
limitNoCap the number of results returned (default 500 when omitted; count still reports the total matches)
placeNoPlace-name substring(s) from reverse geocoding (city, region, landmark). NOTE: multiple values are ANDed, not ORed — a photo must match every value
titleNoSubstring match on title
videoNoOnly videos/movies (alias of movies)
folderNoFolder name(s)/path(s) — matches photos in albums that live inside the folder; ANY-match (see list-folders for names)
hiddenNoOnly hidden photos
moviesNoInclude movies
personNoPerson name(s); ANY-match
photosNoInclude still photos
selfieNoOnly selfies (front-camera photos)
slowMoNoOnly slow-motion videos
toDateNoISO 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
keywordNoKeyword(s); ANY-match
libraryNoPath to a .photoslibrary (default: system Photos library)
maxSizeNoOriginal file size at most this many bytes
minSizeNoOriginal file size at least this many bytes
favoriteNoOnly favorites
fromDateNoISO 8601 lower bound on photo date
minScoreNoOnly 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
panoramaNoOnly panoramas
portraitNoOnly portrait-mode (depth-effect) photos
noKeywordNoOnly photos carrying no keyword at all
notHiddenNoExclude hidden photos (default behavior)
timelapseNoOnly time-lapse videos
addedAfterNoISO 8601 inclusive lower bound on IMPORT date (dateAdded — when the photo entered the library, not when it was taken)
screenshotNoOnly screenshots
addedBeforeNoISO 8601 upper bound on IMPORT date. A bare date includes that whole day; a full datetime is a precise exclusive bound
addedInLastNoImported 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"
descriptionNoSubstring match on description
hasLocationNotrue = only photos WITH GPS coordinates; false = only photos WITHOUT; omit for no location filter
newestFirstNoSort matches by taken date, newest first, BEFORE limit is applied — so limit means 'the N most recent matches' instead of 'N in database order'
notFavoriteNoExclude favorites
detectedTextNoCase-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
screenRecordingNoOnly screen recordings

Output Schema

ParametersJSON Schema
NameRequiredDescription
countNo
photosNo
returnedNo

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. It covers result-count semantics (count vs returned, default limit 500), hidden-photo exclusion default, newestFirst+limit ordering, post-filter behavior for minScore and detectedText with performance advice, place-value ANDing, and the GPS location requirement. These go well beyond the schema and give the agent a clear model of tool behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Although long, every section ('Use when', 'Returns', 'Do not use when') earns its place for a tool with 41 parameters and important routing decisions. The purpose is front-loaded, the filter list is comprehensive but integrated, and the exclusion guidance is compactly placed at the end. No filler or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a read-only query tool with no annotations, the description is complete: it tells when to use it, when not to, what the response looks like (count, returned, photo summary fields), default behaviors, edge cases (hidden, location, post-filters), and downstream actions for the returned UUIDs. An agent has everything needed to invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3. The schema itself already documents each parameter with rich notes (e.g., limit defaults, place ANDing, near GPS requirement, post-filter behavior). The description adds a high-level filter inventory and a few usage patterns, but for most parameters it does not materially add meaning beyond the schema, so it stays at baseline.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource ('find photos matching one or more filters') and enumerates the filter dimensions. It explicitly names itself the primary search/discovery tool ('start here when you don't already have a UUID') and differentiates from siblings in the 'Do not use when' section (get-photo/get-photos, get-thumbnail, list-albums/list-keywords/list-persons), so an agent can select it correctly without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description has explicit 'Use when' and 'Do not use when' sections that name alternative tools and the conditions that route to them. It also gives practical guidance like passing newestFirst=true with a limit to get the N most recent matches, and tells the user to feed returned UUIDs into get-photo/get-photos, get-thumbnail, or export.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
uuidYesPhoto UUID(s) to remove from the album (1–100)
albumYesAlbum name or UUID (UUID-looking values try the id lookup first)

Output Schema

ParametersJSON Schema
NameRequiredDescription
albumNo
removedNo
notInAlbumNo
removedCountNo
albumRecreatedNo
previousAlbumUuidNo

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description fully discloses the WRITE nature, the APPLE_PHOTOS_MCP_ENABLE_WRITES requirement, the never-deletes-photos guarantee, the album rebuild behavior (UUID change, lost sort order), the scratch-name mechanism, mid-rebuild failure effects, and the max 100 UUIDs. It also notes the macOS Automation permission and AppleScript drive. This is comprehensive.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the primary use case, followed by returns, exclusions, and safety details. It is organized into labeled sections and every sentence adds value, justifying its length given the tool's complexity. No fluff or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity (write operation, side effects, environment requirements, failure modes) and the presence of an output schema, the description covers all essential aspects: return values, side effects, edge cases (no-op UUIDs, interrupted runs), prerequisites, and limitations. An agent has everything needed to call it correctly and safely.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and both parameters already have detailed descriptions (album accepts name or UUID with id-lookup behavior; uuid array limited to 1–100). The tool description adds no new parameter meaning beyond reinforcing the max and the album lookup, so it meets the baseline but does not exceed it.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb ('take photos OUT of an album'), the exact resource (album membership), and the intent (undoing mis-filing, clearing quarantine). It explicitly differentiates from sibling tools like add-to-album and clarifies it does NOT delete photos, making the purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

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 alternatives (quarantine in album, review in Photos.app) and conditions (photos not in album, deletion intent). This leaves no ambiguity about when to call this tool versus others.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
addNoKeywords to add (created in Photos if new)
uuidYesPhoto UUID (hex-with-dashes, as returned by query)
removeNoKeywords to remove from this photo (exact match)

Output Schema

ParametersJSON Schema
NameRequiredDescription
uuidNo
addedNo
afterNo
beforeNo
changedNo
removedNo

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full burden and does so exceptionally: it flags the WRITE nature, the environment variable gate, UNION semantics (no blind replace), metadata-only operation, pre-validation, AppleScript dependency, and permission requirement. This is comprehensive disclosure.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is structured with clear sections (Use when, Do not use when, Safety) and every sentence adds value. It is dense but not rambling, 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.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a write tool with side effects and external dependencies, it explains return fields (uuid, before/after, added/removed, changed), safety gates, and operational prerequisites. Nothing essential is missing for an agent to call it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% but the description adds meaningful semantics: 'adding an existing keyword is a no-op', 'exact match' for remove, and the rejection of a keyword in both arrays. These are not in the schema and materially affect invocation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states a specific verb ('add and/or remove keywords') and resource ('on a photo'), and explicitly distinguishes itself from siblings like list-keywords and query. It also provides use cases (tagging workflows, fixing mis-tags) which immediately conveys purpose.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

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 name the alternatives (list-keywords, query) and the conditions that route to them. It also warns about a specific edge case (keyword in both add and remove) which is actionable guidance.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
dateNoAbsolute 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
uuidYesPhoto UUID (hex-with-dashes, as returned by query)
dryRunNoDefault TRUE: preview the before/after dates without writing anything. Pass false to actually write the new date
shiftSecondsNoShift the current date by this many seconds (negative = earlier; e.g. -86400 = one day back). Exactly one of date / shiftSeconds

Output Schema

ParametersJSON Schema
NameRequiredDescription
uuidNo
afterNo
beforeNo
dryRunNo
appliedNo
shiftSecondsNo

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden and does so thoroughly: dry-run default, explicit write only when dryRun=false, library-database-only modification, no EXIF changes, timezone handling, validation, AppleScript permissions, and the required APPLE_PHOTOS_MCP_ENABLE_WRITES flag.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but well-structured with labeled sections and front-loaded critical warnings. Minor redundancy exists between the 'DRY RUN BY DEFAULT' statement and the later safety note that nothing is written unless dryRun=false, but the length is justified given the tool's write risk and operational complexity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex write tool with no annotations, the description is exceptionally complete: preconditions, permissions, safety gating, exact behavior, return field explanation, revert path, timezone handling, and exclusions are all covered. An agent has everything needed to preview, write, and undo the operation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, but the description adds meaning beyond the schema: exactly one of date/shiftSeconds must be provided, dry runs return would-be dates, re-running with date=<echoed before> reverts, and shiftSeconds semantics are reinforced. It also clarifies timezone interpretation for the date parameter.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific purpose: fixing a photo's date/time, with concrete examples of when this is needed. It clearly distinguishes this tool from query (finding by date) and from EXIF-modifying operations, so an agent can tell exactly what resource and operation are involved.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly states 'Use when' and 'Do not use when', names the query tool as the alternative for date-based searching, and warns against expecting EXIF changes. It also mandates the exact selection between date and shiftSeconds, leaving no ambiguity about invocation intent.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
uuidYesPhoto UUID (hex-with-dashes, as returned by query)
titleNoNew title (empty string clears it)
favoriteNoSet or clear the favorite flag
descriptionNoNew description (empty string clears it)

Output Schema

ParametersJSON Schema
NameRequiredDescription
uuidNo
afterNo
beforeNo
updatedNo

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden, and it delivers thoroughly: it declares WRITE semantics, the APPLE_PHOTOS_MCP_ENABLE_WRITES=1 gate, the fact that only passed fields change, empty-string clearing behavior, pre-validation of the photo, AppleScript/Automation permission needs, and the target-library context. It even explains the return payload's revert value.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is substantial but every sentence earns its place: context, return value, exclusions, safety, side effects, and prerequisites. It is front-loaded with the decision-relevant 'Use when' and 'Do not use when' sections, and the bolding/scanning aids agent parsing.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

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 return shape, revert mechanism, exclusions to sibling tools, environmental enablement, permissions, side-effect boundaries, and validation behavior. There is no missing operational context an agent would need to invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already documents all parameters at 100% coverage, so the baseline is 3. The description adds meaningful behavioral semantics beyond the schema: an empty string clears title/description, only the fields you pass are modified, and favorite is set or cleared. This exceeds the schema baseline without duplicating it.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names a specific verb (set), a specific resource (photo metadata), and the exact fields affected: title, description, and favorite flag. It also distinguishes itself from the siblings set-keywords and get-photo in the 'Do not use when' section, 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.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly states when to use the tool ('Use when'), when not to use it ('Do not use when'), and names the exact alternatives (set-keywords, get-photo) with the reason for choosing them. It also includes environment-enablement and permission prerequisites, leaving no ambiguity about call conditions.

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.

  1. 21 tool updatesv2.1.13
    • Changedadd-to-album2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedcreate-album2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changeddoctor2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedexport2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedfind-duplicates2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedget-photo2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedget-photos2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedget-selected-photos2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedget-thumbnail2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedhealth-check2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedimport-photos2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedlibrary-info2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedlist-albums2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedlist-folders2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedlist-keywords2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedlist-persons2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedquery2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedremove-from-album2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedset-keywords2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedset-photo-date2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedset-photo-metadata2 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
  2. 21 tool updatesv2.1.9
    • Changedadd-to-album1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedcreate-album1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changeddoctor1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedexport1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedfind-duplicates1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedget-photo1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedget-photos1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedget-selected-photos1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedget-thumbnail1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedhealth-check1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedimport-photos1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedlibrary-info1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedlist-albums1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedlist-folders1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedlist-keywords1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedlist-persons1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedquery1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedremove-from-album1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedset-keywords1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedset-photo-date1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
    • Changedset-photo-metadata1 field changed
      • changedOutput schema / additionalProperties
        Previous value: -falseNew value: +true
  3. 5 tool updatesv2.1.0
    • Changedget-photo1 field changed
      • addedInput schema / properties / burstPhotos
        Added 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"
        +}
    • Addedget-selected-photos
    • Addedimport-photos
    • Changedquery3 fields changed
      • addedInput schema / properties / detectedText
        Added 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"
        +}
      • addedInput schema / properties / minScore
        Added 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"
        +}
      • addedInput schema / properties / near
        Added 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"
        +}
    • Addedset-photo-date
  4. 5 tool updatesv2.0.0
    • Addedadd-to-album
    • Addedcreate-album
    • Addedremove-from-album
    • Addedset-keywords
    • Addedset-photo-metadata
  5. 4 tool updatesv1.5.0
    • Addedfind-duplicates
    • Addedget-photos
    • Addedget-thumbnail
    • Changedquery22 fields changed
      • addedInput schema / properties / addedAfter
        Added 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"
        +}
      • addedInput schema / properties / addedBefore
        Added 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"
        +}
      • addedInput schema / properties / addedInLast
        Added 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"
        +}
      • addedInput schema / properties / burst
        Added value: +{
        +  "description": "Only burst photos",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / folder
        Added 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"
        +}
      • addedInput schema / properties / hasLocation
        Added value: +{
        +  "description": "true = only photos WITH GPS coordinates; false = only photos WITHOUT; omit for no location filter",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / label
        Added 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"
        +}
      • addedInput schema / properties / live
        Added value: +{
        +  "description": "Only live photos",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / maxSize
        Added value: +{
        +  "description": "Original file size at most this many bytes",
        +  "exclusiveMinimum": 0,
        +  "type": "integer"
        +}
      • addedInput schema / properties / minSize
        Added value: +{
        +  "description": "Original file size at least this many bytes",
        +  "exclusiveMinimum": 0,
        +  "type": "integer"
        +}
      • addedInput schema / properties / newestFirst
        Added 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"
        +}
      • addedInput schema / properties / noKeyword
        Added value: +{
        +  "description": "Only photos carrying no keyword at all",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / panorama
        Added value: +{
        +  "description": "Only panoramas",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / place
        Added 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"
        +}
      • addedInput schema / properties / portrait
        Added value: +{
        +  "description": "Only portrait-mode (depth-effect) photos",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / screenRecording
        Added value: +{
        +  "description": "Only screen recordings",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / screenshot
        Added value: +{
        +  "description": "Only screenshots",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / selfie
        Added value: +{
        +  "description": "Only selfies (front-camera photos)",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / slowMo
        Added value: +{
        +  "description": "Only slow-motion videos",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / timelapse
        Added value: +{
        +  "description": "Only time-lapse videos",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / video
        Added value: +{
        +  "description": "Only videos/movies (alias of movies)",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / year
        Added value: +{
        +  "description": "Taken in calendar year(s); ANY-match (e.g. [2024, 2025])",
        +  "items": {
        +    "maximum": 9999,
        +    "minimum": 0,
        +    "type": "integer"
        +  },
        +  "maxItems": 100,
        +  "type": "array"
        +}
  6. 2 tool updatesv1.4.0
    • Changedexport2 fields changed
      • changedInput schema / properties / dest / description
        Previous value: -"Destination directory (created if missing)"New value: +"Destination directory (created if missing). Must be under the home directory, /tmp, /private/tmp, or /Volumes"
      • addedInput schema / properties / dest / minLength
        Added value: +1
    • Changedget-photo3 fields changed
      • changedInput schema / properties / uuid / description
        Previous value: -"Photo UUID"New value: +"Photo UUID (hex-with-dashes, as returned by query)"
      • addedInput schema / properties / uuid / maxLength
        Added value: +256
      • addedInput schema / properties / uuid / pattern
        Added value: +"^[0-9A-Fa-f-]+$"
  7. 1 tool updatev1.2.0
    • Changedquery3 fields changed
      • changedInput schema / properties / limit / description
        Previous value: -"Cap the number of results"New value: +"Cap the number of results returned (default 500 when omitted; count still reports the total matches)"
      • changedInput schema / properties / toDate / description
        Previous 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"
      • addedOutput schema / properties / returned
        Added value: +{
        +  "type": "number"
        +}
  8. 8 tool updatesv1.1.3
    • Changedexport4 fields changed
      • addedInput schema / properties / dest / maxLength
        Added value: +4096
      • addedInput schema / properties / library / maxLength
        Added value: +4096
      • addedInput schema / properties / uuid / items / maxLength
        Added value: +256
      • addedInput schema / properties / uuid / maxItems
        Added value: +1000
    • Changedget-photo1 field changed
      • addedInput schema / properties / library / maxLength
        Added value: +4096
    • Changedlibrary-info1 field changed
      • addedInput schema / properties / library / maxLength
        Added value: +4096
    • Changedlist-albums1 field changed
      • addedInput schema / properties / library / maxLength
        Added value: +4096
    • Changedlist-folders1 field changed
      • addedInput schema / properties / library / maxLength
        Added value: +4096
    • Changedlist-keywords2 fields changed
      • addedInput schema / properties / library / maxLength
        Added value: +4096
      • addedInput schema / properties / limit / maximum
        Added value: +100000
    • Changedlist-persons2 fields changed
      • addedInput schema / properties / library / maxLength
        Added value: +4096
      • addedInput schema / properties / limit / maximum
        Added value: +100000
    • Changedquery14 fields changed
      • addedInput schema / properties / album / items / maxLength
        Added value: +1024
      • addedInput schema / properties / album / maxItems
        Added value: +100
      • addedInput schema / properties / description / maxLength
        Added value: +2048
      • addedInput schema / properties / fromDate / maxLength
        Added value: +64
      • addedInput schema / properties / keyword / items / maxLength
        Added value: +1024
      • addedInput schema / properties / keyword / maxItems
        Added value: +100
      • addedInput schema / properties / library / maxLength
        Added value: +4096
      • addedInput schema / properties / limit / maximum
        Added value: +100000
      • addedInput schema / properties / person / items / maxLength
        Added value: +1024
      • addedInput schema / properties / person / maxItems
        Added value: +100
      • addedInput schema / properties / title / maxLength
        Added value: +1024
      • addedInput schema / properties / toDate / maxLength
        Added value: +64
      • addedInput schema / properties / uuid / items / maxLength
        Added value: +256
      • addedInput schema / properties / uuid / maxItems
        Added value: +1000
  9. 10 tool updatesv1.1.0
    • Addeddoctor
    • Changedexport1 field changed
      • changedOutput 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"
        +}
    • Changedget-photo1 field changed
      • changedOutput 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"
        +}
    • Changedhealth-check1 field changed
      • changedOutput 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"
        +}
    • Changedlibrary-info1 field changed
      • changedOutput 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"
        +}
    • Changedlist-albums1 field changed
      • changedOutput 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"
        +}
    • Changedlist-folders1 field changed
      • changedOutput 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"
        +}
    • Changedlist-keywords1 field changed
      • changedOutput 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"
        +}
    • Changedlist-persons1 field changed
      • changedOutput 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"
        +}
    • Changedquery1 field changed
      • changedOutput 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"
        +}
  10. 9 tool updatesv0.1.4
    • First observedexport
    • First observedget-photo
    • First observedhealth-check
    • First observedlibrary-info
    • First observedlist-albums
    • First observedlist-folders
    • First observedlist-keywords
    • First observedlist-persons
    • First observedquery

TDQS

A4.5/5.0

Scored across 21 tools

Disambiguation5/5

Each tool targets a distinct purpose: query finds photos, get-photo/get-photos retrieve metadata, get-thumbnail displays images, export copies files, list-* functions enumerate catalogs, and set-* tools modify specific attributes. Even seemingly similar pairs like health-check vs doctor are clearly differentiated by scope (smoke test vs full diagnostic) and explicit 'Do not use when' guidance.

Naming Consistency5/5

The tool names follow a consistent snake_case verb_noun pattern (get-photo, list-albums, set-keywords, create-album, add-to-album, import-photos). The only deviations are health-check and doctor, which are noun-style names, but they are both diagnostic tools and the pattern remains clear throughout the set.

Tool Count4/5

With 21 tools, this is on the higher end of the typical range, but each tool earns its place given the comprehensive scope of managing a Photos library (search, metadata, export, import, album manipulation, diagnostics). The count is justified by the breadth of functionality, though a few tools (e.g., get-photos vs get-photo) could theoretically be merged without losing clarity.

Completeness4/5

The tool surface covers the primary lifecycle: query, read metadata, view thumbnails, export, import, album creation and membership, and metadata/keyword/date updates. The main gaps are intentional (no photo deletion due to AppleScript limitations) and minor (no folder creation/deletion, no album deletion). These are acceptable given the stated purpose and platform constraints.

Maintenance

ActivityActive
ResponsivenessWithin a week

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    This MCP server enables AI tools to interact with your Apple Photos library via the osxphotos CLI, providing tools for querying and managing photos.
    2
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables users to search and retrieve photos from a self-hosted Immich photo library via natural language, supporting CLIP-based semantic search, metadata filtering, album browsing, and share link creation.
    -