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, backed by the osxphotos library.

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

Read-only against the Photos library. Exports write files to a directory you choose, but the library itself is never modified.

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"

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

  • "List my albums"

  • "Tell me everything about photo UUID ABC-123"

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: Google Photos MCP Server

Quick Start

Using Claude Code (Easiest)

If you're using Claude Code (in Terminal or VS Code), just ask Claude to install it:

Install the sweetrb/apple-photos-mcp MCP server so you can help me query my Apple Photos 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)

Configuration for two more hosts is included — each registers the same apple-photos MCP server (npx -y apple-photos-mcp). As a Python-sidecar (osxphotos) server it also needs Full Disk Access; see Requirements.

Manual Installation

1. Install the server:

npm install -g apple-photos-mcp

2. Python deps install automatically. The first tool call auto-bootstraps a project-local Python venv with osxphotos (a one-time setup that can take ~a minute; progress is logged to stderr). You do not need to install anything by hand.

To skip the first-call delay, you can pre-warm the venv ahead of time:

pnpm run setup   # optional — pre-installs osxphotos so the first tool call is instant

Auto-setup needs Python 3, pip, and network access. If any are missing — or you disabled auto-setup via APPLE_PHOTOS_MCP_NO_AUTO_SETUP=1 — run pnpm run setup (or pip3 install osxphotos) yourself. See Configuration and Troubleshooting.

3. Add to Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "mcpServers": {
    "apple-photos": {
      "command": "npx",
      "args": ["apple-photos-mcp"]
    }
  }
}

4. Grant Full Disk Access to the app hosting the MCP server (Claude Desktop, Terminal, VS Code, etc.) — see Full Disk Access below.

5. Restart Claude Desktop and start using natural language:

"How many photos are in my library?"

Requirements

  • macOS - The Photos library is macOS-only

  • Node.js 20+ - Required for the MCP server

  • Python 3.11+ - The server uses osxphotos under the hood and installs it automatically on first use into a project-local venv (one-time, ~a minute). You only need Python 3.11+, pip, and a network connection available. osxphotos requires Python ≥ 3.10 and the date filters need 3.11; macOS ships 3.9, so install a newer Python first (e.g. brew install 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 date range, album, keyword, person, favorite/hidden flags, photo/movie type, title/description substring

Photo Details

Full metadata for one photo: dimensions, location, place, EXIF-derived flags (HDR, live, portrait, panorama, raw, edited, etc.)

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

Diagnostics

Feature

Description

Health Check

Verify osxphotos is installed and the library can be opened

Doctor

Richer setup diagnostic — five checks: Python interpreter (path + version), osxphotos install, sidecar mode (persistent vs one-shot fallback), 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.


Tool Reference

This section documents all available tools. AI agents should use these tool names and parameters exactly as specified.

Discovery

health-check

Verify osxphotos is installed and the Photos library can be opened.

Parameters: None

Returns: osxphotos version, library path, and total photo count — or an error if the library is inaccessible.


doctor

Run a full setup diagnostic: the resolved Python interpreter (path + version — warns when it's older than the required 3.11, with brew install python@3.12 advice), osxphotos installation, Photos library readability, and Full Disk Access — each reported as ok / warn / fail with an actionable message. This is the richer counterpart to health-check; reach for it first when a tool returns a permission or "unable to open" error.

Parameters: None

Returns: A per-check report. The structuredContent carries the raw { healthy, checks[] }, where each check has name, status (ok/warn/fail), and detail. The Full Disk Access check explicitly reports whether the host process can read the library — see Full Disk Access.


library-info

High-level stats about the Photos library.

Parameter

Type

Required

Description

library

string

No

Path to a non-default .photoslibrary (defaults to system library)

Returns: Library path, Photos DB version, Photos.app version, counts of photos / movies / albums / folders / keywords / persons.


Query

query

Search the library with combinable filters. Returns photo summaries with UUIDs — use get-photo for full details on a specific match.

For the full filter syntax — accepted date forms, AND/OR combination semantics, exact-vs-substring matching, result ordering, and what is not filterable — see the Query Guide.

Parameter

Type

Required

Description

uuid

string[]

No

Specific UUIDs to fetch (max 1000 entries, each ≤ 256 chars)

album

string[]

No

Album name(s); ANY-match, exact full folder path + name (max 100 entries)

keyword

string[]

No

Keyword(s); ANY-match, exact whole-string (max 100 entries)

person

string[]

No

Person name(s); ANY-match, exact whole-string (max 100 entries)

fromDate

string

No

ISO 8601 inclusive lower bound on photo date (e.g. "2025-06-01")

toDate

string

No

ISO 8601 upper bound on photo date. A bare date (e.g. "2025-06-30") includes that whole day; a full datetime (e.g. "2025-06-30T18:00:00") is a precise exclusive bound

favorite

boolean

No

Only favorites

notFavorite

boolean

No

Exclude favorites

hidden

boolean

No

Only hidden photos

notHidden

boolean

No

Exclude hidden photos (default behavior)

photos

boolean

No

Include still photos

movies

boolean

No

Include movies

title

string

No

Substring match on title (case-sensitive, ≤ 1024 chars)

description

string

No

Substring match on description (case-sensitive, ≤ 2048 chars)

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"]
}

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)

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


Browse

list-albums

List all albums in the library.

Parameter

Type

Required

Description

library

string

No

Path to a non-default .photoslibrary

Returns: Each album's title, folder path, photo count, shared status, and UUID. iCloud Shared Albums are included and flagged isShared: true.


list-folders

List all folders in the library.

Parameter

Type

Required

Description

library

string

No

Path to a non-default .photoslibrary

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


list-keywords

List keywords sorted by usage count.

Parameter

Type

Required

Description

limit

number

No

Cap to top-N keywords

library

string

No

Path to a non-default .photoslibrary

Returns: Keywords with their photo counts, sorted descending.


list-persons

List people detected by Photos face recognition, sorted by photo count.

Parameter

Type

Required

Description

limit

number

No

Cap to top-N persons

library

string

No

Path to a non-default .photoslibrary

Returns: Persons with their photo counts, sorted descending. Unidentified faces appear as _UNKNOWN_.


Export

export

Export one or more photos by UUID to a destination directory.

Parameter

Type

Required

Description

uuid

string[]

Yes

Photo UUID(s) to export (1–1000 entries, each ≤ 256 chars)

dest

string

Yes

Destination directory (created if missing). Must resolve — after expanding ~ and following symlinks — to a path under your home directory, /tmp, /private/tmp, or /Volumes; anything else is rejected

edited

boolean

No

Export the edited version instead of the original

live

boolean

No

Also export the live-photo video

raw

boolean

No

Also export the raw image

overwrite

boolean

No

Overwrite existing files at the destination. Without it, a photo whose file already exists is skipped (reported per-UUID) — never duplicated

library

string

No

Path to a non-default .photoslibrary

Example - Originals to a folder:

{
  "uuid": ["33AC0410-...", "EEFCEF1D-..."],
  "dest": "~/Desktop/exports"
}

Example - Edited versions plus raw and live-photo video:

{
  "uuid": ["33AC0410-..."],
  "dest": "~/Desktop/exports",
  "edited": true,
  "raw": true,
  "live": true,
  "overwrite": true
}

Returns: Destination path, count of files exported, count skipped, list of exported file paths, and a per-UUID reason for every skip (file already exists, UUID not found / in Recently Deleted, iCloud download failed, ...). Every requested UUID is accounted for in exported + skipped. Note that export reads the main library only: a photo in Recently Deleted is skipped with UUID not found (deleted or in trash) even though get-photo still resolves it (that tool falls back to the trash).

Destination allowlist: the destination is canonicalized (leading ~ expanded, .. normalized, symlinks resolved — including a not-yet-existing final directory) and must land under the home directory, /tmp, /private/tmp, or /Volumes. The check is segment-aware (/Volumesx does not pass as /Volumes), and the canonical path is what's exported into, so a symlink under an allowed root can't redirect the write outside it.

Filename collisions: Files keep the photo's original filename. If a file of that name already exists at the destination and overwrite is not set, the photo is skipped with reason already exists at destination — re-running an export never creates IMG_1234 (1).jpg-style duplicates. Pass overwrite: true to replace in place.

iCloud-only originals: If a photo's original isn't on disk (Photos is using "Optimize Mac Storage"), the export automatically falls back to Photos.app via AppleScript, which downloads the original on demand — same behavior as opening the photo in Photos. This is slower than a direct file copy; expect waits proportional to download size for large batches. Photos that genuinely can't be exported (e.g. edited=true requested but no edits exist) are still skipped with a per-UUID reason.

Progress notifications: For batch exports, the server emits one MCP progress notification per photo (progress/total plus a message naming the file being exported) when the client's request includes a progressToken — so hosts that surface progress can show a live counter instead of a silent multi-minute call. Clients that don't send a token simply get the final result, as before. (Progress requires the persistent sidecar; in the rare one-shot fallback mode the export still works but reports no intermediate progress.)


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

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 setupoptional; 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_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". export keeps its own 30-minute window.

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

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.

  • 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 against the Photos library — the library is never modified.

  • Exports write to diskexport 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

osxphotos reads the Photos library; this server does not modify it

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

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.

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.

Install Server
A
license - permissive license
A
quality
A
maintenance

Maintenance

Maintainers
5dResponse time
1dRelease cycle
10Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sweetrb/apple-photos-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server