immich-library-mcp
MCP server for an Immich photo library (built against Immich v3.3.3). It lets an agent search the library by content (CLIP smart search) or by metadata, read EXIF/people/album/tag detail, count and map the library, look at the photos themselves (preview/thumbnail images and numbered contact sheets returned as MCP image content), analyse it (duplicates, largest files, stacks, activity, processing queues), export browser-ready files plus a metadata manifest, stage originals on a shared mount for other MCP servers and, with a second key, organise it (favourites, ratings, descriptions, dates, locations, tags, albums, people, uploads, reprocessing).
Two keys, two trust levels:
IMMICH_API_KEY(always): read-only. Scope it to read permissions only, so Immich itself refuses writes (the integration suite asserts a403on album create). With only this key the server is read-only: the curation tools are not registered at all, and the only writes are local files (the shared mount and the export folder).IMMICH_WRITE_API_KEY(optional): curation. Scoped to exactly the 13 write permissions the curation tools use and no read permissions, so every lookup still goes through the read key. It cannot delete or trash photos, share anything, or touch admin settings (the integration suite asserts403s on/server/about,DELETE /assetsandPOST /shared-links). Give it only to clients that should organise the library; a shared HTTP deployment (chat front ends, automation) can run without it.
Install
uvx immich-library-mcp --stdio # or: pip install immich-library-mcp
The distribution is immich-library-mcp (the name immich-mcp on PyPI belongs to
another project); the import package is immich_mcp, and both immich-library-mcp
and immich-mcp commands are installed. Claude Desktop / Claude Code (stdio):
{"mcpServers": {"immich": {"command": "uvx", "args": ["immich-library-mcp", "--stdio"],
"env": {"IMMICH_URL": "http://localhost:2283", "IMMICH_API_KEY": "<read-only key>"}}}}
Tools
All tools take at least one parameter (Claude.ai's remote client rejects
empty schemas). Every read tool carries readOnlyHint. The local file
writers (immich_download, immich_export, immich_export_cleanup) and
the curation tools don't; immich_export_cleanup, immich_delete_album,
immich_delete_tag and immich_merge_people are marked destructiveHint
(and prompt for confirmation through the fleet's MCP guard).
Reading
| Tool | What it does | Key parameters |
|---|---|---|
immich_search |
Find assets. query → CLIP smart search; empty → metadata browse by capture date |
query, people (comma-sep, AND), album, tags, city/state/country, camera_make/camera_model, media_type any|photo|video, favorite_only, year or taken_after/taken_before (YYYY, YYYY-MM, YYYY-MM-DD), text_in_image (OCR), filename/description (browse only), visibility timeline|archive|hidden|all, order newest|oldest, limit ≤100, page |
immich_count |
How many assets match | same filters (no query); group_by total|year|month (grouped = timeline: one person/album/tag, favourite, year, visibility) |
immich_get_asset |
Full metadata: EXIF, GPS + place, people, tags, albums, live-photo link, OCR text | asset_id, include_ocr |
immich_view |
Returns the images (with caption lines) so the model can see them | asset_ids (list), size preview (JPEG, 1440 px short side, ≤6) | thumbnail (~250 px WebP, ≤20) |
immich_contact_sheet |
One grid JPEG of up to 50 thumbnails, each stamped with its index (VID = video), plus a text legend index → id, date, place, people |
asset_ids (≤50), columns (0 = auto, ≤10), tile_px 100–400 |
immich_people |
Named people (face clusters), optional asset counts | name (lookup), include_unnamed, include_hidden, with_counts, limit |
immich_albums |
Albums with counts and date span | name_filter, sort name|newest|size |
immich_places |
Geotagged counts per country/state/city | level, country, year/taken_after/taken_before, favorite_only, limit |
immich_memories |
"On this day" memories | on_date (YYYY-MM-DD, default today), limit_per_memory |
immich_overview |
Library summary (version, totals, span, people, albums, tags, countries) | include_lists |
immich_download |
Save one original/fullsize/preview: on the shared mount (shared:<filename>, for other MCP servers) or in the export folder |
asset_id, size, destination shared|export, subfolder (export only) |
immich_export |
Export up to 200 assets as browser-ready files + manifest.json into EXPORT_DIR/<folder> (details below) |
asset_ids, folder, size, format, max_edge_px, quality, filename_pattern, include_video |
immich_export_cleanup |
Delete export folders/files whose newest file is older than N days | older_than_days, dry_run |
Library analysis (read)
| Tool | What it does | Key parameters |
|---|---|---|
immich_duplicates |
Immich's duplicate groups (visually near-identical), with sizes, dimensions, the suggested keeper and reclaimable space | limit ≤200, sort wasted|copies |
immich_large_assets |
Biggest files first | media_type, min_size_mb, year/taken_after/taken_before, city, country, limit ≤100 (no people/album filter: Immich 3.3's endpoint mis-parses list and boolean filters) |
immich_random |
Random assets, narrowed by the search filters | count ≤50, people, album, tags, city, country, media_type, favorite_only, year/taken_after/taken_before |
immich_stacks |
Bursts / RAW+JPEG pairs: list stacks, or every member of one asset's stack | asset_id, limit |
immich_activity |
Photos per day: total, active days, busiest days, per-month totals (every day when ≤62 days) | start/end (YYYY-MM-DD, default last 365 days), by taken|upload |
immich_processing_status |
Background queues (faces, OCR, smart search, thumbnails …): waiting/active/failed | include_idle |
immich_folders |
Folders Immich stores originals under, or the assets in one | path, contains, limit |
immich_album |
One album: description, count, span, users, order, cover, top places, first N assets | album (name or id), include_assets ≤100 |
immich_person |
One person: flags, asset count, link, and their face thumbnail as an image | person (name or id), include_face |
immich_get_asset also returns Immich's per-asset key/value metadata, the
stack it belongs to and its duplicate group.
Organising (only with IMMICH_WRITE_API_KEY)
| Tool | What it does | Key parameters |
|---|---|---|
immich_update_assets |
Change up to 200 assets; only the fields passed change. Returns each asset's previous values so the change can be put back | asset_ids, favorite, archived, rating (1–5, -1 rejected, 0 clear), description ('' clears), taken (YYYY-MM-DD HH:MM[:SS] local) + time_zone (IANA), or shift_minutes, latitude + longitude |
immich_tag_assets |
Tag (creating missing tags; Parent/Child nests) or untag up to 1000 assets |
asset_ids, tags (comma-sep), remove |
immich_delete_tag |
Delete a tag everywhere (assets untouched); reports how many had it. Destructive, asks first | tag |
immich_create_album |
New private album, optionally with assets; flags a name clash | name, description, asset_ids ≤1000 |
immich_update_album |
Rename, describe, set cover or sort order | album, name, description, cover_asset_id, order oldest|newest |
immich_album_assets |
Add/remove assets (removing keeps them in the library) | album, add, remove (≤1000 each) |
immich_delete_album |
Delete an album; its photos stay. Destructive, asks first | album |
immich_update_person |
Name/rename a face cluster, birth date, hide, favourite, face-thumbnail photo; flags an existing person with the same name | person, name, birth_date, hidden, favorite, cover_asset_id |
immich_merge_people |
Move every face of merge into into. Cannot be undone. Destructive, asks first |
into, merge (comma-sep) |
immich_upload |
Upload from the shared mount (shared:<file>) or export folder (export:<folder>/<file>); files Immich already has come back duplicate (SHA-1 checksum) |
source, album, favorite, archived, description |
immich_reprocess |
Queue refresh-faces | refresh-metadata | regenerate-thumbnail | transcode-video for ≤500 assets |
asset_ids, job |
Edits never modify original files: Immich keeps them in its database and
an XMP sidecar. immich_upload only reads from the two hand-off folders
(symlinks out of them are refused) and only accepts the extensions Immich
lists in /server/media-types.
Names (people, albums, tags) resolve exact → case-insensitive → unique substring; ambiguous names error with the candidates. UUIDs pass through.
Times are the camera's local wall clock (Immich localDateTime); bare dates
in filters are interpreted in TZ.
Gallery exports
immich_export writes into EXPORT_DIR/<folder> — a persistent, non-hidden
folder (default ~/Pictures/Immich Exports, created at startup) that you can
connect to another tool once and leave connected. Nothing sweeps it; prune with
immich_export_cleanup(older_than_days=…). Because cleanup deletes inside it,
EXPORT_DIR must be a dedicated folder: ~, ~/Pictures, ~/Desktop etc. are
refused.
| Parameter | Default | Notes |
|---|---|---|
asset_ids |
— | Up to 200; duplicates collapsed |
folder |
— | One plain folder name, e.g. china-food-2026-10-16 |
size |
preview |
thumbnail (~250 px) | preview (1440 px short side) | fullsize | original |
format |
jpeg |
jpeg | webp re-encode; keep = bytes exactly as Immich serves them |
max_edge_px |
0 (none) | Downscale only (64–10000); needs jpeg/webp |
quality |
82 | 1–100 |
filename_pattern |
{taken:%Y-%m-%d_%H%M}_{short_id}.{ext} |
Fields: {taken} (datetime, any strftime spec), {short_id}, {id}, {stem} (original name), {index} (1-based input order, e.g. {index:03d}), {city}, {country}, {type}, {ext}. Sanitised to a bare filename; .{ext} appended if missing |
include_video |
false | Videos always export a poster-frame image; this adds Immich's web-playable video (/video/playback, the transcoded MP4) beside it |
Where pixels come from. Immich renditions first: thumbnail/preview are
Immich's own; fullsize follows Immich's redirect (the original for web formats,
the preview where Immich has no full-size rendition). Local conversion
(Pillow + pillow-heif) only fills the gaps: HEIC originals at fullsize/original
are decoded and re-encoded locally. Camera RAW (ARW, NEF …) can't be decoded
here, so it falls back to Immich's preview, flagged in that asset's note
(turn on Immich's Full-size image generation for true full-res RAW).
Re-encoded files keep the colour profile (iPhone Display P3) and EXIF
orientation, and drop all other metadata, GPS included — safe to publish.
keep files are untouched, so originals keep their EXIF.
Dedupe. If the folder's manifest already has the asset with the same
settings and its file still matches the recorded sha256, it is skipped with
no download (unchanged). Otherwise new bytes identical to the file on disk
are not rewritten. A name clash with another asset, or with a file no
manifest owns (your own edits), gets a -2 suffix rather than being
overwritten. If a re-export renames an asset's file (new pattern or format),
its old file is removed.
manifest.json is a JSON array, one entry per exported asset, sorted by capture time. It accumulates across runs (an asset re-exported replaces its entry):
{
"asset_id": "a1b2c3d4-…", "file": "2009-12-02_0135_a1b2c3d4.jpg",
"width": 1920, "height": 1440, "bytes": 285982, "sha256": "5f1d…",
"taken": "2009-12-02T01:35:46", "place": "Kyoto, Japan", "city": "Kyoto",
"country": "Japan", "camera": "Canon IXY DIGITAL 10", "people": ["…"],
"immich_url": "https://photos.example.com/photos/a1b2c3d4-…",
"type": "image", "duration_s": null, "variant": "preview/jpeg/0/q82"
}
Videos add video_file, video_bytes, video_sha256 when exported with
include_video. variant records the export settings (for dedupe).
immich_url is the plain web link; the API key never appears in any URL,
filename or manifest.
The tool returns the absolute folder and manifest paths, count,
total_bytes, by_status and per-asset {asset_id, status, file, note?, error?} with status exported | updated | unchanged | failed.
Failures are per asset; the rest of the batch still exports. Downloads run 5
at a time, with at most 2 large decodes in parallel.
Configuration
| Env | Default | Notes |
|---|---|---|
IMMICH_URL |
http://immich:2283 |
Server base URL, no /api |
IMMICH_API_KEY |
— | Scope read-only: asset.read asset.view asset.download asset.statistics album.read album.statistics person.read person.statistics tag.read timeline.read map.read map.search memory.read server.about user.read duplicate.read stack.read queue.read folder.read |
IMMICH_WRITE_API_KEY |
— (curation tools off) | Optional. Scope to exactly asset.update asset.upload tag.create tag.asset tag.delete album.create album.update album.delete albumAsset.create albumAsset.delete person.update person.merge job.create |
IMMICH_PUBLIC_URL |
IMMICH_URL |
Browser-facing base for the url on every row; set it when the server reaches Immich by an internal address |
SHARED_DIR |
/shared |
Cross-MCP mount for hand-offs to other MCP servers |
EXPORT_DIR |
~/Pictures/Immich Exports |
Persistent export root (immich_export, immich_download(destination='export')); must be a dedicated folder |
MAX_DOWNLOAD_BYTES |
2 GiB | Cap per streamed file (originals, videos) |
IMMICH_TIMEOUT |
60 | Seconds; cold smart search pays a CLIP model load |
TZ |
UTC | For date filters |
PORT |
8336 | HTTP mode |
MCP_BEARER_TOKEN |
— | Required for HTTP mode (fails closed) |
Running
# stdio (interactive clients; no network surface, no bearer)
uvx --env-file .env immich-library-mcp --stdio
# Streamable HTTP at /mcp (stateless, bearer-authenticated)
MCP_BEARER_TOKEN=... IMMICH_API_KEY=... immich-mcp
# container
podman build -t immich-library-mcp .
Sync tool bodies run in a worker thread (one slow upstream call never
stalls the event loop) and each call logs tool=X outcome=Y duration_ms=N
to stderr. Argument values are never logged (httpx request logging is
silenced because query strings carry names).
Testing
uv run --extra test pytest -q # tier 1 + 2 (mocked)
IMMICH_TEST=1 uv run --env-file .env \
--extra test pytest tests/test_integration.py -v # tier 3, live
The live curation tests run only when IMMICH_WRITE_API_KEY is in the env
too. They change one real photo and one named person and restore every
value in a finally (favourite, rating, description, a +60/−60 minute time
shift, location), and create and delete their own album and tag. Merging
people is never run live.
tests/test_formatting.py— pure helpers (date bounds, rows, aggregation, name matching)tests/test_client.py— request shapes, errors, caching, streaming downloadtests/test_server.py— every tool through a mocked Immich, schema/annotation and bearer checkstests/test_library.py— the library-analysis tools (duplicates, large assets, random, stacks, activity, queues, folders, album, person)tests/test_curation.py— curation tools: request bodies, validation sends nothing, previous-value capture, upload path confinement, which key each request carries, registration only with the write keytests/test_export.py— HEIC/JPEG/WebP encoding (orientation, ICC kept, EXIF/GPS stripped), filename patterns, name claiming, manifest, cleanup, contact sheettests/test_gallery.py—immich_exportend to end against real image bytes and Immich's fullsize 302s: manifest schema, dedupe, HEIC/RAW paths, video, per-asset failures, collisions, cross-origin redirect refusaltests/test_integration.py— live: scope probes (403), search → detail → view, counts, places, download, HEIC + video export round trip, contact sheet, library analysis; with the write key, the restore-everything curation round trips
Metadata
Release files for immich-library-mcp 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| immich_library_mcp-0.1.0.tar.gz | 123.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| immich_library_mcp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 172.1 kB
Release files / immich_library_mcp-0.1.0.tar.gz
| Download URL | immich_library_mcp-0.1.0.tar.gz |
|---|---|
| Size | 123.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c265a610d43d494352791ceeeadb015b6e318f352b6a88cfea8f31d2ac90cec1
|
|
BLAKE2b-256 checksum How to use checksums |
0313d68af1e96d9806bf774c446ee3d80538a746262f7590d4a0b5e06ef2402b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / immich_library_mcp-0.1.0-py3-none-any.whl
| Download URL | immich_library_mcp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 48.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
caaf24858370085dd4492a01c4a892dde94b36fbdefb5f3037fb52c2ff9e4b36
|
|
BLAKE2b-256 checksum How to use checksums |
9c8e8c5c590660a77b8b362c9713047c98b131495aba66671342585ff1654d5c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for immich-library-mcp 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.