Skip to main content
Glama

gphotos-mcp

A read-only MCP server that lets Claude list, search, inspect and look at the photos in one personal Google Photos library. It talks to the same internal batchexecute RPC that photos.google.com itself uses, through gpwc, and signs in with a cookies.txt exported from your browser. Nothing in it can modify the library.

Why the unofficial web API

On 31 March 2025 Google removed the Photos Library API scopes that gave apps access to a user's existing media (photoslibrary.readonly and friends); what remains is the Picker API and access to media an app uploaded itself. Reading your whole library programmatically today means using the web app's private RPC, which is what this server does.

Related MCP server: Google Maps MCP Server

Tools

tool

arguments

returns

list_library

page_token?, limit=50

newest-first by taken date: media_key, filename, taken_at (ISO 8601 with the item's own UTC offset), type (photo/video), width, height; plus next_page_token

search

query, limit=50, page_token?

same item shape, using Google Photos' own search (people, places, things, text in images, dates)

list_albums

album_key, title, item_count, cover_thumbnail_url, is_shared

get_album

album_key, page_token?, limit=50

album title / count and its items, same item shape

get_media_info

media_key

what the web UI's info panel shows: taken / uploaded time, dimensions, file size, camera (raw), location (decimal degrees + place name), description, albums it belongs to, upload source, favourite / archived flags, storage quality

get_media

media_key, max_px=1024

the picture as MCP image content (JPEG, longest side ≤ max_px, quality 80); for a video the poster frame plus a note that it is a video

download_media

media_key, dest_dir

saves the original file to disk, returns path, filename, bytesstdio mode only

get_storage_quota

used_bytes, total_bytes, photos_bytes

The item shape is identical across list_library, search and get_album. docs/PAYLOADS.md maps every tool to the RPC behind it.

Requirements

  • Python 3.11+ and uv — or Docker with Compose v2.

  • A browser to export cookies from, and (for the claude.ai connector) a way to expose one port, e.g. cloudflared or Tailscale Funnel.

Cookies (authentication)

The server authenticates exactly like your browser does. Export the cookies from a dedicated incognito session that you never open again, as described in the gpwc README:

  1. Install the Get cookies.txt LOCALLY extension and allow it in private / incognito windows.

  2. Open a new incognito window and sign in to Google.

  3. Open photos.google.com and go to your library page.

  4. Open a new empty tab, then close the Google Photos tab.

  5. Click Export All Cookies in the extension and save the file as cookies.txt.

  6. Close the incognito window and do not reopen that session in a browser.

Keep the file private (chmod 600 cookies.txt): it is equivalent to your signed-in browser. When Google invalidates the session every tool fails with Google session expired — re-export cookies.txt; export a fresh file to the same path and the server picks it up on the next call (no restart needed).

If the browser session held several Google accounts, GP_ACCOUNT_INDEX selects one (the N in photos.google.com/u/N/).

Configuration

All settings are environment variables; copy .env.example to .env.

variable

default

meaning

GP_COOKIES_FILE

(required)

path to the Netscape cookies.txt

GP_ACCOUNT_INDEX

0

account within the cookie session

GP_CACHE_DIR

~/.cache/gphotos-mcp

cache of downscaled JPEGs, keyed by (media_key, max_px)

MCP_TRANSPORT

stdio

stdio or streamable-http

MCP_HOST

127.0.0.1

bind address in http mode (0.0.0.0 inside Docker)

MCP_PORT

8765

port in http mode

MCP_AUTH_TOKEN

(required in http mode)

≥ 16 URL-safe characters; openssl rand -hex 32

In http mode the server refuses to start without MCP_AUTH_TOKEN. The token is accepted either as Authorization: Bearer <token> or as a secret path prefix, https://host/<token>/mcp, for connector UIs that cannot set headers. Cookies, tokens and request bodies are never logged (uvicorn's access log is off for that reason).

Running

Native

uv sync
GP_COOKIES_FILE=/path/to/cookies.txt uv run gphotos-mcp          # stdio
MCP_TRANSPORT=streamable-http MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
GP_COOKIES_FILE=/path/to/cookies.txt uv run gphotos-mcp          # http on 127.0.0.1:8765/mcp

Docker compose

cp .env.example .env           # set GP_COOKIES_FILE (host path) and MCP_AUTH_TOKEN
docker compose up -d --build   # Streamable HTTP on 127.0.0.1:8765
docker compose logs -f

The compose service always runs in http mode, publishes the port on localhost only, mounts the cookies file read-only at /config/cookies.txt, keeps the image cache in a named volume and maps ./downloads to /downloads. The container runs as uid 1000, so on Linux the cookies file must be readable by that uid.

For stdio through Docker (Claude Desktop / Claude Code on the same machine):

docker compose run --rm -i -e MCP_TRANSPORT=stdio gphotos-mcp

download_media then writes inside the container: pass dest_dir=/downloads to land in ./downloads on the host.

Claude Desktop and Claude Code (stdio)

Claude Desktop: add to claude_desktop_config.json (macOS ~/Library/Application Support/Claude/, Windows %APPDATA%\Claude\). Claude Code: the same object in .mcp.json (project) or ~/.claude.json (user), or run claude mcp add gphotos -e GP_COOKIES_FILE=/abs/path/cookies.txt -- uv run --directory /abs/path/google-photos-mcp gphotos-mcp.

{
  "mcpServers": {
    "gphotos": {
      "command": "uv",
      "args": ["run", "--directory", "/abs/path/google-photos-mcp", "gphotos-mcp"],
      "env": { "GP_COOKIES_FILE": "/abs/path/cookies.txt" }
    }
  }
}

Docker variant (needs .env next to the compose file):

{
  "mcpServers": {
    "gphotos": {
      "command": "docker",
      "args": ["compose", "-f", "/abs/path/google-photos-mcp/docker-compose.yml",
               "run", "--rm", "-i", "-e", "MCP_TRANSPORT=stdio", "gphotos-mcp"]
    }
  }
}

claude.ai custom connector (http mode)

claude.ai can only reach a public HTTPS URL, so put a tunnel in front of the server. Keep the port bound to localhost and let the tunnel do the exposure; the token is the only access control.

  1. Set MCP_AUTH_TOKEN in .env and start the server: docker compose up -d --build.

  2. Expose it, for example:

    • cloudflared quick tunnel: cloudflared tunnel --url http://127.0.0.1:8765 prints a https://….trycloudflare.com hostname (it changes on every run; use a named tunnel for a stable one), or

    • Tailscale Funnel: tailscale funnel 8765 gives https://<machine>.<tailnet>.ts.net.

  3. In claude.ai open Settings → Connectors → Add custom connector and enter https://<public-host>/<MCP_AUTH_TOKEN>/mcp. Leave the OAuth client fields empty. Clients that can send headers may instead use https://<public-host>/mcp with Authorization: Bearer <MCP_AUTH_TOKEN>.

The secret is part of the URL, so it will appear in the tunnel provider's logs and in the connector settings. Rotate it by changing MCP_AUTH_TOKEN and restarting. download_media is not offered in http mode because the server's disk is not the client's.

How it treats Google

Google's abuse detection is the main operational risk, so the server is deliberately slow:

  • exactly one request in flight at a time, at least 300 ms apart, no parallelism anywhere;

  • 429 / 5xx / network errors are retried at most 3 times with jittered exponential backoff;

  • get_media results are cached on disk, so re-viewing an image costs nothing;

  • list calls cost two RPCs per page (the list itself, then one batch call for filenames); get_media_info and download_media batch their two RPCs into one HTTP request.

Risks

  • Terms of service. This uses Google's private web API with your own session. It is a grey area; automated access can get an account rate-limited or flagged. Use it for yourself, keep volumes small, and do not run several instances against one account.

  • It will break. The RPC ids and array layouts are undocumented and change without notice. When that happens the parsers in src/gphotos_mcp/rpc.py need updating; docs/PAYLOADS.md records where every index came from.

  • The cookies are your account. Anyone holding cookies.txt can act as you on Google. Store it like a password and export it from a session you do not use for anything else.

  • The http token is a bearer secret. Whoever has the URL has read access to your photos.

Not supported

  • Anything that writes: delete, trash, move, edit, favourite, album changes, uploads (uploads are gpmc's job). Not even behind a flag.

  • The official Library / Picker APIs and OAuth.

  • cover_media_key for albums: the web API only exposes the cover as a thumbnail URL.

  • Structured EXIF (aperture, exposure, ISO, focal length): the info RPC carries one opaque camera value whose layout is undocumented; it is passed through as-is.

  • Trash, archive, locked folder, partner sharing, shared links, the video half of Live Photos.

  • Bulk operations, more than one account per server instance, download_media in http mode.

Development

uv sync                                          # includes the dev group (pytest)
uv run pytest                                    # unit tests, no network, recorded fixtures
GP_COOKIES_FILE=cookies.txt uv run python scripts/smoke.py --query dog   # live check of every tool

Layout: src/gphotos_mcp/client.py is the adapter (plain dataclasses in and out; gpwc is used for the session and request payloads), rpc.py parses the raw arrays, transport.py holds the pacing / retry / session-expiry logic, and server.py only maps tools onto the adapter. tests/fixtures/ are sanitized RPC responses (see the README there).

Dependencies: mcp 2.x (its high-level server class was called FastMCP in 1.x and is MCPServer in 2.x; same decorator API), gpwc pinned to a git commit, and pillow. httpx is not used directly: every HTTP call goes through gpwc's requests session so the cookie jar, timeouts, pacing and retries live in one place (gpwc's own tests download the same way).

Credits

xob0t reverse-engineered the protocol in google_photos_web_client and Google-Photos-Toolkit; the recorded fixtures derive from the latter (MIT). Background on batchexecute: Deciphering Google's batchexecute.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables read-only inspection of Google Ads accounts through MCP, including account inventory, reporting, metadata, change history, and safe GAQL queries.
    Apache 2.0