Skip to content

feat(adapters): add read-only Immich adapter (#1238) - #1329

Merged
helpcodeai merged 5 commits into
HelpCode-ai:mainfrom
GallaGit:adapter/immich
Oct 29, 2026
Merged

helpcodeai merged 5 commits into
HelpCode-ai:mainfrom
GallaGit:adapter/immich

Conversation

@GallaGit

Copy link
Copy Markdown
Contributor

Summary

Adds a read-only adapter for Immich, the self-hosted photo and video library, as asked in #1238. An AI can count the photos and videos, list albums and who they are shared with, read one asset's dates, EXIF, GPS, people and tags, search people and places, and get the number of assets per month. All 8 tools are GET requests on the Immich 3.x REST API ({{IMMICH_URL}}/api), authenticated with an API key in the x-api-key header.

As you suggested, the setup does not need an admin account. The instructions list the 5 key permissions (user.read, asset.read, asset.statistics, album.read, person.read) as the way to keep access small. The live run used a non-admin user's key with exactly those 5 permissions.

Tested against Immich v3.3.5 (GET /api/server/version returns 3.3.5). It ran locally with the official v3.3.5 docker-compose.yml. The test library had 22 JPEGs: 1 in the trash, 1 archived, 1 tag, 1 owned album and 1 album shared by another user, plus 1 recognised and named person.

Changes

  • packages/backend/src/adapters/intl/immich.json: 8 GET tools, instructions, envVarMeta for IMMICH_URL / IMMICH_API_KEY, verifyHints (401/403/404), probe immich_get_my_user, healthcheck /server/ping.

    Tool Endpoint Response mapping
    immich_get_my_user GET /users/me exclude license, oauthId, profileImagePath
    immich_get_asset_statistics GET /assets/statistics none
    immich_list_albums GET /albums JMESPath: id, albumName, description, assetCount, shared, startDate, endDate, albumThumbnailAssetId, owner (name), members (name, role)
    immich_get_album GET /albums/{id} exclude albumUsers[*].user.profileImagePath
    immich_get_asset GET /assets/{id} exclude originalPath, checksum, thumbhash, owner.profileImagePath, people[*].thumbnailPath
    immich_search_people GET /search/person exclude [*].thumbnailPath
    immich_get_timeline_buckets GET /timeline/buckets none (tagId kept; the id comes from immich_get_asset().tags[].id)
    immich_search_places GET /search/places none
  • immich.live.spec.ts: 21 static tests and 9 live tests, gated by RUN_IMMICH_LIVE=1. The fake key is built at runtime, so there are no secrets in fixtures.

  • content/guides/en/immich-to-mcp.mdx: the setup guide.

  • packages/frontend/public/logos/connectors/immich.svg: Simple Icons 16.36.0 (CC0), with fill="#4250AF", following AGENTS.md step 6.

  • I regenerated catalog.ts, raised the adapter counts from 562 to 563, and regenerated llms.txt / llms-full.txt.

Decisions taken from the live run and reflected in the descriptions and instructions:

  • visibility enum is timeline | archive | hidden. locked is left out because an API key gets 401 Elevated permission is required for it.
  • Unknown ids return 400, not 404. Immich answers 400 Not found or no asset.read access for an unknown asset or album id, and the instructions say so.
  • Out of scope:
    • smart and metadata search (both are POST requests; a follow-up could use a readOnlyHint override)
    • uploads and any write
    • originals, thumbnails and video downloads
    • the locked folder
    • shared links, memories, admin and API-key endpoints

Type

  • Bug fix
  • New feature
  • Documentation
  • Refactoring
  • Performance improvement
  • Other (describe below)

Testing

  • Added/updated unit tests
  • Tested manually (describe steps below)
  • No tests needed (explain why)
Directory Command Result
repo root node scripts/validate-adapters.mjs --warn 577 passed, 0 failed; no warning for immich.json
repo root node --test scripts/validate-adapters.test.mjs 29 pass, 0 fail
repo root node scripts/regenerate-catalog.mjs catalog regenerated (+2 lines for immich)
repo root node scripts/adapter-count.mjs --check OK: 563 adapters (27 keyless, 8689 tools)
repo root node scripts/generate-llms-txt.mjs llms.txt, llms-full.txt regenerated
packages/backend npx jest src/adapters/intl/immich 21 passed, 9 skipped (live)
packages/backend RUN_IMMICH_LIVE=1 IMMICH_URL=http://localhost:2283 IMMICH_API_KEY=... IMMICH_API_KEY_NO_PERSON=... IMMICH_PERSON_NAME=Alex npx jest src/adapters/intl/immich 30 passed, 30 total (Immich v3.3.5)
packages/backend npx jest --maxWorkers=3 (full backend suite) 797 suites passed, 4 failed, 4 skipped; 23696 tests passed, 7 failed

Baseline failures. These 4 failing suites fail the same way on a clean upstream/main (aae3cc3) in my environment (7 failed, 42 passed). None of them involves this change:

  • sentry-scrub.spec.ts and secret-guard.flows.spec.ts: TypeScript errors on SpanJSON.name / .attributes with my installed @sentry types.
  • ssrf.util.spec.ts: "a name that does not resolve" depends on my DNS resolver.
  • cloudflare.live.spec.ts: its live gate picked up a Cloudflare variable from my shell.

Live behaviour measured on v3.3.5:

What Result
/assets/statistics without filters 21 of 22 assets. The trashed asset is excluded and the archived one is included.
visibility=timeline / archive / isTrashed=true 20 / 1 / 1
/timeline/buckets without filters Same rule: trash excluded, archive included. One {timeBucket, count} per month, newest first, timeBucket = YYYY-MM-01 (e.g. 2008-10-01). personId, albumId and tagId filters work.
visibility=locked with an API key 401 Elevated permission is required
No key / wrong key 401 Authentication required / 401 Invalid API key
Key without person.read on /search/person 403 Missing required permission: person.read
Admin endpoint with the user key 403 Forbidden (the key is not admin)
Non-UUID id 400 Validation failed (Invalid UUID)
Unknown asset / album UUID 400 Not found or no asset.read access / no album.read access (not 404)
/search/person or /search/places without name 400 Validation failed
IMMICH_URL ending in /api 404 Cannot GET /api/api/users/me
/albums/{id} no assets array (members, assetCount, date range only)
/server/ping without a key {"res":"pong"}

Response sizes, raw vs. after the response mapping (small test library):

Tool Raw Mapped
immich_get_my_user 496 B 446 B
immich_list_albums (2 albums) 1,433 B 558 B (about 700 B → 280 B per album)
immich_get_album 690 B 668 B
immich_get_asset 1,715 B 1,494 B
immich_search_people (1 person) 324 B 208 B
immich_get_timeline_buckets (10 months) 381 B 381 B
immich_search_places ("Berlin", 20 results) 2,276 B 2,276 B

/albums grows with every album and every member, so it is the only list I shaped with JMESPath. The other answers are a single object or already small.

Checklist

  • My code follows the existing code style
  • I have tested my changes locally
  • I have updated documentation (if applicable)
  • All existing tests pass (npm test): all pass except the 4 baseline suites above, which also fail on clean main
  • This PR has a descriptive title

Related Issues

Closes #1238. Part of #997.

Open option for the maintainer

Without filters, immich_get_asset_statistics and immich_get_timeline_buckets mirror the Immich API: trashed assets are excluded but archived ones are counted. I left that behaviour unchanged and documented it in the instructions. If you prefer the counts to match what the Immich app timeline shows, I can default visibility to timeline in a follow-up commit.

Eight GET-only tools on the Immich 3.x REST API with an x-api-key key
that needs only user.read, asset.read, asset.statistics, album.read and
person.read, no admin account. Responses drop file paths, checksums,
thumbhashes, OAuth ids and license keys. Updates the catalog, adapter
counts and llms.txt.
…#1238)

Static checks pin auth, read-only tools, URLs and the response
mappings. Live checks, verified against Immich v3.3.5, cover the default
trash and archive counts, the monthly bucket format, the 400/401/403
codes and log the raw and mapped response sizes.

@helpcodeai helpcodeai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @GallaGit, this follows everything we agreed on in #1238: license, oauthId, thumbnailPath, profileImagePath, thumbhash, originalPath and checksum are excluded, all 8 tools are GET, the key is marked secret, the instructions say no admin account is needed and list the 5 permissions, and the Simple Icons logo is included. Security check: no script or event attributes in the SVG, and the live suite only runs behind RUN_IMMICH_LIVE=1. Merging.

@helpcodeai helpcodeai mentioned this pull request Oct 29, 2026
7 of 14 tasks
@helpcodeai
helpcodeai enabled auto-merge (squash) October 29, 2026 15:54
@helpcodeai
helpcodeai merged commit fca0ea1 into HelpCode-ai:main Oct 29, 2026
14 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 29, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🎃 New adapter: Immich (self-hosted photo library, read-only)

2 participants