---
name: photo-search
description: >
Search and explore an Immich photo library using natural language, GPS locations,
dates, people, cameras, and AI-powered visual search (CLIP).
Use when the user says "find photos of", "search my photos", "show me pictures from",
"where are my photos of", "do I have photos of", "find all screenshots",
"photos taken with", "photos from 2019", "photos near", "photos of [person]",
or any variation of searching, browsing, or exploring their photo library.
version: 1.2.0
---
# Photo Search
## Connection Required — ALWAYS CHECK FIRST
**Before doing ANYTHING else in this skill, call `ping` on the Immich MCP server.**
- If `ping` succeeds -> proceed with the skill normally.
- If `ping` fails or the MCP tools are not available -> **STOP. Do not continue.** Tell the user:
> Immich is not connected. This plugin needs a running Immich MCP server to work.
>
> Run **/setup-immich-photo-manager** to configure your Immich connection. You'll need:
> 1. Your Immich server URL (e.g., `http://192.168.1.100:2283`)
> 2. An Immich API key ([how to create one](https://immich.app/docs/features/command-line-interface#obtain-the-api-key))
> 3. The MCP server configured (see **/setup-immich-photo-manager**)
**Do NOT skip this check. Do NOT try to run any other tool first. Always ping, always block if it fails.**
---
## CRITICAL RULE: NEVER CREATE TEMPORARY ALBUMS
**This is the most important rule of this skill. NEVER create albums as part of a search workflow.**
The user has a curated library of real albums. Creating temporary albums pollutes their library with junk. Instead:
- If photos belong to a real album -> use that real album directly
- If photos are NOT in any album -> show them directly using `get_thumbnails_batch`
- **NEVER call `create_album` from this skill. Not for "temp" albums, not for "search results", not for any reason.**
---
## Search Workflow (Step by Step)
### Step 1: Parse user intent
Identify what the user is looking for. Determine which search dimensions apply.
### Step 2: Search for matching photos
Use `search_metadata` and/or `search_smart` to find matching assets.
**IMPORTANT — Immich EXIF location quirks:**
- Immich stores cities as **municipalities**, not tourist names. "Tikal" does not exist as a city — it's in the municipality of **"Flores"** (state: **"Petén"**, country: **"Guatemala"**).
- "Lanzarote" does not exist as a city — look for municipalities like "Arrecife", "Yaiza", "Teguise", "Haría", etc. (state: **"Canary Islands"**, country: **"Spain"**).
- When a place-name search returns 0 results, try broader terms: search by **state** or **country** instead of city, then filter. Or use `search_smart` with the place name as a CLIP query.
Search strategy priority:
1. `search_metadata(city=...)` — fastest, most precise if the city name matches
2. `search_metadata(state=...)` or `search_metadata(country=...)` — broader, catches municipalities
3. `search_smart(query="...")` — AI/CLIP semantic search, catches things without GPS
### Step 3: Find REAL matching albums
Call `list_albums()` and **fuzzy-match** album names/descriptions against the user's query.
Examples:
- User asks "photos of Tikal" -> match album "Tikal & Petén" (and possibly "Guatemala")
- User asks "fotos de la Barceloneta" -> match album "Barcelona — Barceloneta / Playa"
- User asks "Valle del Jerte" -> match album "Valle del Jerte & Hervás"
- User asks "Lanzarote" -> match albums containing "Lanzarote" in their name
Matching rules:
- Case-insensitive substring match on album name
- Also check album descriptions
- The query terms can appear anywhere in the album name (e.g., "Tikal" matches "Tikal & Petén")
- Include albums for broader regions if relevant (e.g., for "Tikal" also include "Guatemala")
### Step 4: Collect asset metadata for the gallery
**Two paths depending on whether a real album was found:**
#### Path A — Real album found (preferred)
Use `get_album(album_id)` to get the full asset list. Extract `id`, `originalFileName`, and `fileCreatedAt` from each asset.
This is the best path because the user curated these albums intentionally.
#### Path B — No matching album (orphan photos)
Use the search results directly — they already contain `id`, `originalFileName`, and `fileCreatedAt` for each asset.
**Do NOT create an album. Just show the photos.**
**Fetch thumbnails as base64** using `get_thumbnails_batch(asset_ids=[...], size="thumbnail", limit=50)`. The Cowork viewer runs in an `about:` sandbox that blocks ALL external network requests, so thumbnails MUST be embedded as base64 `data:` URIs.
### Step 5: Build and present the HTML gallery
1. Read the template: `assets/viewer-template.html` from the plugin root
2. Replace all `{{PLACEHOLDERS}}` with actual data
3. Write the HTML to the outputs directory
4. Present via `computer://` link
**For Related Albums ({{ALBUMS_JSON}}):**
- Include ONLY real albums found in Step 3
- If no real albums matched, use an empty array `[]`
- NEVER fabricate album entries
---
## Search Capabilities
| Dimension | MCP Tool / Parameter | Example |
|-----------|---------------------|---------|
| **Visual/semantic** | `search_smart(query=...)` | "sunset at the beach", "birthday cake" |
| **Location (text)** | `search_metadata(city=..., state=..., country=...)` | city="Barcelona" |
| **Date range** | `search_metadata(taken_after=..., taken_before=...)` | 2023-06-01 to 2023-06-30 |
| **Camera/device** | `search_metadata(make=..., model=...)` | make="Apple", model="iPhone 14 Pro" |
| **File type** | `search_metadata(asset_type=...)` | "IMAGE" or "VIDEO" |
| **Favorites** | `search_metadata(is_favorite=true)` | true |
## Query Translation
| User says | Search strategy |
|-----------|----------------|
| "photos from my Italy trip" | `search_metadata(country="Italy")` + `list_albums()` to find Italy albums |
| "sunset photos" | `search_smart(query="sunset")` |
| "photos from last Christmas" | `search_metadata(taken_after="2025-12-20", taken_before="2025-12-31")` |
| "my best photos" | `search_metadata(is_favorite=true)` |
| "photos taken with iPhone" | `search_metadata(make="Apple")` |
| "videos from Barcelona" | `search_metadata(city="Barcelona", asset_type="VIDEO")` |
| "show me Tikal" | `search_metadata(state="Petén")` + `search_smart(query="Tikal")` + match album "Tikal & Petén" |
---
## Gallery HTML Generation
### Template
Use the canonical template at `assets/viewer-template.html`. Read the template file, replace `{{PLACEHOLDERS}}` with actual data, and write the result.
### Placeholder Rules
- **`{{PAGE_SIZE}}`**, **`{{PHOTO_COUNT}}`**, **`{{ALBUM_TOTAL}}`**: Should be plain integers (e.g. `20`). The template uses `parseInt()` with fallbacks, so non-numeric values degrade gracefully (PAGE_SIZE defaults to 6, others to 0).
- **`{{ALBUM_NAME}}`**: Can contain any characters including apostrophes (e.g. "L'Hospitalet"). Safe in HTML contexts. The JS alt-text reads from `document.title` instead of re-injecting this placeholder, so apostrophes won't break JS.
- **`{{SEARCH_QUERY}}`**, **`{{IMMICH_URL}}`**: Can be any string.
- **`{{PHOTO_ENTRIES}}`**: Must be valid JS object literals, comma-separated.
- **`{{ALBUMS_JSON}}`**: JSON album objects. The template wraps them in `[...].flat()`, so you can pass any of these formats:
- Comma-separated objects: `{"id":"abc","name":"X","total":50},{"id":"def","name":"Y","total":30}`
- A JSON array: `[{"id":"abc","name":"X","total":50}]`
- Empty string (no albums): the template produces `[].flat()` → `[]` and hides the section
### Thumbnail Delivery Strategies
There are three strategies for delivering thumbnails to the gallery viewer, with different trade-offs. The strategy used depends on the user's Immich setup and the viewing context.
#### Strategy 1: Base64 Embedded (Default — Always Works)
The Cowork viewer runs in an `about:` protocol sandbox that blocks ALL external network requests (`fetch`, `
`, etc.). Therefore, the **default and always-safe strategy** is to embed thumbnails as base64 `data:` URIs directly in the HTML.
Each photo entry in `{{PHOTO_ENTRIES}}` includes the full thumbnail data:
```javascript
{src:'data:image/jpeg;base64,/9j/4AAQ...',id:'',name:'',date:''}
```
- `src`: Base64 data URI of the thumbnail (from `get_thumbnails_batch`, size=thumbnail, ~250px, ~15-25KB each)
- `id`: The Immich asset ID (for linking to Immich web UI)
- `name`: Original filename (displayed as label)
- `date`: ISO date string from the asset metadata
**Always use `size="thumbnail"` (250px)** — never `preview` (1440px). Thumbnails average ~18KB each, so 50 photos ≈ 0.9MB HTML file.
**How to get thumbnails:** Call `get_thumbnails_batch(asset_ids=[...], size="thumbnail", limit=50)`. If more than 50 photos, call in batches of 50.
**Limitations:** HTML file size grows linearly with photo count (~18KB per photo). Not ideal for albums with hundreds of photos. Maximum practical limit is ~50 thumbnails per gallery file.
#### CORS-Enabled Direct URLs (Optional — Requires User Setup)
If the user has configured CORS headers on their Immich reverse proxy, the gallery HTML can use JavaScript `fetch()` with the `x-api-key` header to load thumbnails on demand, converting responses to blob URLs. This enables full URL-based delivery for **any** photos, not just albums.
**This requires the user to configure their reverse proxy** (Nginx, Caddy, Traefik, etc.) to return CORS headers. See the **Post-Install: CORS Configuration** section in `/setup-immich-photo-manager` for instructions.
**Advantages:** Works for any photos (albums or search results), tiny HTML file, true on-demand pagination, no shared links needed.
**Limitations:** Requires CORS configuration on the server side. Not available out of the box.
#### Strategy Decision Flow
```
Always use Base64 Embedded (Strategy 1) as the default:
├─ ≤50 photos → Embed all thumbnails as base64
└─ >50 photos → Embed first 50 in batches, warn user about file size and total count
```
**Note:** CORS-enabled direct URLs (Strategy 3) are an opt-in enhancement for users who open galleries in a regular browser. They do NOT work inside the Cowork sandbox. Never mention strategy numbers or internal labels in user-facing output — just generate the gallery silently using the correct approach.
### Template lazy loading
The first page (PAGE_SIZE photos) loads immediately. Subsequent pages use IntersectionObserver to set `src` from `dataset.src` only when scrolled into view. Pagination is manual via "Load more" button (no infinite scroll). This works with both base64 and URL-based strategies.
### Albums JSON Format
`{{ALBUMS_JSON}}` — a JSON array of REAL albums:
```
{"id":"abc123","name":"Tikal & Petén","total":169},{"id":"xyz789","name":"Guatemala","total":392}
```
Comma-separated JSON objects — NO outer array brackets (the template adds `[...]`). If no real albums match, use empty string.
### Generation Workflow (Concrete Example)
```
User: "show me photos of Tikal"
1. ping() -> OK
2. search_metadata(state="Petén", country="Guatemala") -> found 200+ assets
3. list_albums() -> scan names -> found "Tikal & Petén" (id: d6dd63d0, 169 photos), "Guatemala" (id: 8dde4bb1, 392 photos)
4. get_album(album_id="d6dd63d0") -> get asset list with IDs, names, dates
5. get_thumbnails_batch(asset_ids=[...], size="thumbnail", limit=50) -> base64 JPEG data for first 50 photos
6. Read assets/viewer-template.html
7. Replace placeholders:
- {{ALBUM_NAME}} -> "Tikal & Petén"
- {{ALBUM_TOTAL}} -> 169
- {{SEARCH_QUERY}} -> "Tikal"
- {{IMMICH_URL}} -> "https://your-immich-server.com"
- {{PAGE_SIZE}} -> 20
- {{PHOTO_COUNT}} -> 50 (limited to 50 for file size)
- {{PHOTO_ENTRIES}} -> {src:'data:image/jpeg;base64,...',id:"abc",name:"IMG_001",date:"2023-06-15"}, ...
- {{ALBUMS_JSON}} -> {"id":"d6dd63d0","name":"Tikal & Petén","total":169},{"id":"8dde4bb1","name":"Guatemala","total":392}
8. Write tikal.html to outputs (~0.9MB with 50 base64 thumbnails, total album has 169 photos)
9. Present computer:// link
```
### When photos are NOT in any album
```
User: "show me sunset photos"
1. ping() -> OK
2. search_smart(query="sunset") -> found 35 assets (each has id, originalFileName, fileCreatedAt)
3. list_albums() -> no album name matches "sunset"
4. Read template
5. Replace placeholders:
- {{ALBUM_NAME}} -> "Sunset Photos"
- {{ALBUM_TOTAL}} -> 35
- {{PHOTO_COUNT}} -> 35
- {{PHOTO_ENTRIES}} -> entries built from get_thumbnails_batch (base64 src + id + name + date)
- {{ALBUMS_JSON}} -> <-- empty string, no real albums match
6. Write sunset-photos.html to outputs (~0.7MB with base64 thumbnails)
7. Present computer:// link
```
---
## Result Presentation
When showing search results:
- **Count first**: "Found 147 photos matching your search"
- **Real albums**: "These photos are in your album 'Tikal & Petén' (169 photos)"
- **Date range**: "Spanning from June 2019 to June 2023"
- **Visual preview**: Always generate the HTML gallery viewer
- **Action prompt**: Suggest next steps (see more photos, explore related albums, etc.)
## Pagination
Immich API returns paginated results. For large result sets:
- Fetch first page to get total count
- Report total to user before fetching all pages
- For browsing, show first page thumbnails and offer to load more
## Advanced Search Patterns
### Finding screenshots
No GPS data + screen-resolution dimensions + no lens/focal length EXIF.
### Finding duplicates
Same date range across import sources. Compare by exact hash, timestamp + dimensions, or CLIP similarity.