immich-mcp
immich-mcp
MCP server for Immich photo management. Provides AI-accessible tools for browsing, searching, organizing, and managing your self-hosted photo library — including duplicate detection and safe deletion workflows.
Features
40+ MCP tools covering assets, albums, search, people, tags, shared links, activities, and duplicates
3 MCP prompts for guided workflows: duplicate review, album organization, and untagged photo discovery
Tool annotations (
title,readOnlyHint,destructiveHint,idempotentHint) for safe AI-driven automationDry-run support on all destructive operations
Duplicate analysis with format priority scoring and protection for favorited/album-linked assets
Automatic retries via
httpx.AsyncHTTPTransport(configurable viaIMMICH_MAX_RETRIES)
Requirements
Python 3.11+
Immich server with API access
uv(recommended) orpip
Installation
git clone https://github.com/whitehara/immich-mcp
cd immich-mcp
uv pip install -e .Configuration
Set environment variables before running:
Variable | Required | Default | Description |
| Yes | — | Immich server URL (e.g. |
| Yes | — | API key from Immich → Account Settings → API Keys |
| No | same as | Public URL used for |
| No |
| HTTP timeout in seconds. The |
| No |
| Retry attempts on transient errors |
Immich API Key Permissions
Immich 1.138.0+ supports granular API key permissions. Grant the following scopes when creating your key (Immich → Account Settings → API Keys → Create).
For full functionality (all tools enabled):
Scope | Required by |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
For read-only use (no create/update/delete tools), grant only:
server.about, user.read, asset.read, asset.statistics, album.read, person.read, person.statistics, tag.read, sharedLink.read, activity.read, activity.statistics, duplicate.read
On Immich versions prior to 1.138.0, select All permissions.
Usage
stdio (Claude Desktop / Claude Code)
export IMMICH_BASE_URL=https://photos.example.com
export IMMICH_API_KEY=your-api-key
immich-mcp --transport stdioClaude Desktop config (claude_desktop_config.json):
{
"mcpServers": {
"immich": {
"command": "immich-mcp",
"args": ["--transport", "stdio"],
"env": {
"IMMICH_BASE_URL": "https://photos.example.com",
"IMMICH_API_KEY": "your-api-key"
}
}
}
}HTTP (Docker / remote)
docker run -e IMMICH_BASE_URL=https://photos.example.com \
-e IMMICH_API_KEY=your-api-key \
-p 8000:8000 \
ghcr.io/whitehara/immich-mcp:latestOr build locally with docker compose up.
Default transport in Docker is streamable-http on port 8000.
Tool Reference
Annotation columns: R = readOnlyHint, D = destructiveHint, I = idempotentHint
Each tool also carries a human-readable title annotation used by MCP clients for display.
Health
Tool | Description | R | D | I |
| Verify connectivity, returns server version | ✓ | ✓ | |
| List supported server features | ✓ | ✓ |
User
Tool | Description | R | D | I |
| Get the authenticated user's profile (name, email, quota, role) | ✓ | ✓ |
Assets
Tool | Description | R | D | I |
| List assets with filters (favorite, visibility, trashed, type), via metadata search | ✓ | ✓ | |
| Get full metadata for a single asset | ✓ | ✓ | |
| Get thumbnail, original, and web UI URLs for an asset | ✓ | ✓ | |
| Upload an asset from a local file path or URL | |||
| Update favorite, archived, description, rating | ✓ | ||
| Bulk update multiple assets (dry_run supported) | ✓ | ||
| Delete assets, optionally permanently (dry_run supported) | ✓ | ✓ | |
| Get counts by asset type | ✓ | ✓ |
v3.1.0 breaking change:
immich_assets_listpreviously called the non-existentGET /api/assetsendpoint and always failed. It now usesPOST /api/search/metadataand returns{count, next_page, items}instead of a bare list.
Search
Tool | Description | R | D | I |
| Filter by date, type, location, camera, person, album, tag, OCR text, filename | ✓ | ||
| Semantic/CLIP search with natural language queries | ✓ | ||
| Discover popular places, people, and things | ✓ | ✓ |
immich_search_metadata and immich_search_smart default visibility to "timeline".
Immich v3 defaults to returning every visibility except "locked" when visibility is
omitted, which would silently mix archived and hidden assets into results; the explicit
default here preserves the previous timeline-only behavior. Pass visibility explicitly
to search a different state.
Albums
Tool | Description | R | D | I |
| List albums, filterable by shared status, ownership, or name | ✓ | ✓ | |
| Get album metadata (see note below) | ✓ | ✓ | |
| Create a new album | |||
| Update album name, description, or cover | ✓ | ||
| Delete an album (assets are not deleted) | ✓ | ✓ | |
| Add assets to an album | ✓ | ||
| Remove assets from an album | ✓ | ||
| Get counts of owned, shared, and not-shared albums | ✓ | ✓ |
v3.1.0 breaking change: immich_albums_list's shared parameter was renamed to
is_shared, and is_owned/name filters were added (Immich v3 renamed the underlying
query param from shared to isShared). immich_albums_get no longer returns the
album's assets or accepts a without_assets parameter — Immich v3's AlbumResponseDto
dropped the assets field entirely. Use immich_search_metadata with album_ids to
list an album's assets instead.
People
Tool | Description | R | D | I |
| List recognized people | ✓ | ✓ | |
| Get person details | ✓ | ✓ | |
| Update name or visibility | ✓ | ||
| Merge two face clusters | ✓ | ||
| Get asset count for a person | ✓ | ✓ | |
| List assets featuring a person (shortcut for | ✓ | ✓ |
Tags
Tool | Description | R | D | I |
| List all tags | ✓ | ✓ | |
| Get a tag by ID | ✓ | ✓ | |
| Create a tag (use | |||
| Update a tag's color | ✓ | ||
| Add assets to a tag | ✓ | ||
| Remove assets from a tag (dry_run supported) | ✓ | ✓ | |
| Assign multiple tags to multiple assets in one call | ✓ | ||
| Delete a tag | ✓ | ✓ |
v3.1.0 breaking change: immich_tags_update no longer accepts a name parameter —
Immich v3's TagUpdateDto only supports updating color. Tag renaming is not supported
by the Immich API.
Shared Links
Tool | Description | R | D | I |
| List all shared links | ✓ | ✓ | |
| Get shared link details | ✓ | ✓ | |
| Create a shareable URL | |||
| Update expiry, password, permissions | ✓ | ||
| Revoke a shared link | ✓ | ✓ |
immich_shared_links_update accepts clear_expires_at: true to remove an existing
expiry and make the link permanent (mutually exclusive with expires_at). expires_at
on both create and update accepts a bare date (2026-12-31) and normalizes it to a
UTC datetime — Immich v3 rejects timezone-less datetime strings outright.
Activities
Tool | Description | R | D | I |
| List comments and likes | ✓ | ✓ | |
| Add a comment or like | |||
| Delete an activity | ✓ | ✓ | |
| Get comment count | ✓ | ✓ |
Duplicates
Tool | Description | R | D | I |
| List duplicate groups with analysis (format score, resolution, protection status). Paginated ( | ✓ | ✓ | |
| Resolve a duplicate group atomically: mark keep/trash and clear the grouping in one call (dry_run supported, recommended over | ✓ | ✓ | |
| Delete assets from duplicate groups (dry_run supported) | ✓ | ✓ | |
| Dismiss duplicate groups without deleting files (dry_run supported) | ✓ | ✓ |
Whether immich_duplicates_resolve's trash_asset_ids moves matched assets to
trash (restorable) or deletes them permanently depends on the target server's
trash feature setting (GET /api/server/features) — Immich v3.1.0's
resolveGroup() skips the trash entirely and deletes files on disk when that
feature is disabled server-wide. The tool checks this automatically on every
non-dry-run call: if the server's trash feature is disabled, the call is
blocked (no HTTP mutation happens) unless allow_permanent_delete=true is
passed explicitly. dry_run=true always reports the detected trash_enabled
value (true/false/null if the check itself failed) without blocking,
so a null result never blocks a subsequent real call — it's treated as
"unknown," not "disabled." If keep/trash from a duplicate group is going to
happen on a server where you haven't confirmed trash is enabled, you can
instead use immich_duplicates_delete(force=false) followed by
immich_duplicates_dismiss — that path always goes to trash regardless of
the server's trash setting. If keep_asset_ids is omitted, every other asset
in the group is kept automatically; Immich requires every asset in a group to
be classified as either kept or trashed.
Pagination
immich_duplicates_list returns paginated results to avoid oversized responses on large libraries:
Parameter | Default | Description |
|
| Page number (1-based) |
|
| Groups per page (max 500) |
The response includes total_groups, total_pages, page, and page_size fields. Iterate page=1..total_pages to process all groups.
Duplicate Analysis Fields
immich_duplicates_list with analyze=true (default) returns per-group analysis:
Field | Description |
| Recommended asset to retain (highest quality) |
| Assets not favorited and not in any album |
| Assets that are favorited or in albums — require explicit user approval |
|
|
| Why review is needed: |
|
|
| Immich's own keep suggestion for the group ( |
Note: while album_membership="unknown", review_reasons contains only album_membership_unknown — a per-asset protected_assets reason (e.g. from favorites) is not added separately, since every delete candidate is already protected wholesale until membership is resolved.
Album membership can't be read off an individual asset — Immich's AssetResponseDto has no albums field — so it's derived by cross-referencing /api/albums with /api/search/metadata in a background task. While that index is still building (album_index_ready=false in the top-level response), every delete candidate in a group is treated as protected (album_membership="unknown", safe_to_delete_ids=[]) rather than risking an unprotected asset that actually belongs to an album. This is conservative by design — protection defaults to "on" until membership is confirmed.
Quality Priority for keep_id (highest to lowest)
Live Photo > Format score > Resolution > File size
Favorites and album membership are tiebreakers only — they do not override a quality difference. A favorited JPEG will not be recommended over an unfavorited RAW; instead the JPEG is listed in protected_ids for user review.
Known limitation: locked assets (visibility="locked") are excluded from the default /api/search/metadata results used to build the album index, so album membership for locked-and-in-an-album assets cannot be detected.
Format Score (highest to lowest)
RAW/DNG > HEIC/HEIF > PNG/TIFF > JPEG > WebP > GIF
Prompt Reference
Invoke prompts from your MCP client to start guided workflows.
immich/review_duplicates
Step-by-step workflow for reviewing and safely resolving duplicate photos:
Fetch duplicate groups with analysis
Present summary: safe-to-delete count vs. protected assets needing review
For each group, choose an action:
Delete: keep the best asset, move the rest to trash (or permanently delete)
Dismiss: assets are intentionally kept as separate copies — remove the duplicate grouping without deleting files
Dry-run confirmation before execution
immich/organize_album
Guided album creation workflow:
Organize by date, event, person, or location
Uses both metadata search and semantic (CLIP) search
Checks for existing albums to avoid duplicates
Confirms asset count before creating
immich/find_untagged
Discover unorganized assets in the library:
Identifies assets with no album, no description, or no recognized people
Suggests descriptions from EXIF data
Hands off to
immich/organize_albumfor album creation
Error Reporting
When Immich returns a non-2xx response, the error message raised back to the MCP client includes the response body (truncated to 2000 characters) — e.g. Immich's Zod validation details on a 400. Request headers (including the API key) are never included.
Safety Model
All destructive tools (delete, merge, remove) carry destructiveHint=true in their MCP annotations, signalling to clients that human confirmation is required before execution. Additional safeguards:
dry_run=trueis the default on all delete operations — no changes occur without explicit opt-inDuplicate analysis places favorited and album-linked assets in
protected_ids; they are never auto-deletedPrompts explicitly instruct the AI to present a summary and obtain user approval before any destructive action
Development
# Install with dev dependencies
uv pip install -e ".[dev]"
# Run tests
pytest
# Run a single test file
pytest tests/tools/test_assets.py -v
# Lint
ruff check .
# Type check (optional)
pyright src/tests/contract/ validates every tool's mocked HTTP calls against a vendored copy of the
Immich OpenAPI spec (method, path, query params, body keys) — run automatically as part of
pytest. tests/live/ smoke-tests read-only tools against a real Immich server; it's
excluded by default (-m "not live") and needs its own env vars so it's never confused
with the mocked test suite's placeholder settings:
IMMICH_LIVE_BASE_URL=https://your-immich-server \
IMMICH_LIVE_API_KEY=your-api-key \
pytest -m liveLatest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/whitehara/immich-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server