lychee-mcp
by si-essence
README.md
# lychee
MCP connector for a self-hosted [Lychee](https://github.com/LycheeOrg/Lychee) photo gallery. It speaks the Lychee `/api/v2` routes over stdio and over stateless Streamable HTTP on localhost.
LycheeOrg/Lychee is MIT software. This connector does not copy the PHP application. It is not affiliated with LycheeOrg.
`lychee.routes.json` lists every uncommented `Route::get|post|put|patch|delete` call in `routes/api_v2.php`. 67 routes are tool actions. 193 are exclusions. `scripts/gen-operations.mjs` writes `src/operations.ts` from that file. Do not edit `src/operations.ts` by hand. Shop, embed, and v3 route files are out of scope. The commented `Photo::getArchive` line is not a route.
`LYCHEE_URL` and `LYCHEE_API_TOKEN` are required. There is no default origin. The client sends `Authorization: Bearer <token>`. Lychee stores a SHA-512 hash of the token and answers HTTP 400 `Invalid token` when the header is wrong.
## Setup
```bash
npm install
npm run ci
npm start
npm run start:http
```
`npm start` speaks stdio. `npm run start:http` serves Streamable HTTP at `http://127.0.0.1:3000/mcp` and `GET /health`. HTTP mode has no authentication of its own. Keep it on localhost.
```json
{
"mcpServers": {
"lychee": {
"command": "node",
"args": ["/workspace/connectors/lychee/dist/index.js"],
"env": {
"LYCHEE_URL": "http://127.0.0.1:8099",
"LYCHEE_API_TOKEN": "...",
"LYCHEE_ALLOW_WRITES": "false",
"LYCHEE_TOOLSETS": "albums,photos,tags,search,sharing,users,settings"
}
}
}
}
```
Docker, HTTP, no authentication, keep it on localhost.
```bash
docker build -t lychee-mcp .
docker run -p 127.0.0.1:3000:3000 \
-e LYCHEE_URL=http://127.0.0.1:8099 \
-e LYCHEE_API_TOKEN=... \
lychee-mcp
```
Do not put secrets in the image.
Regenerate the registry after a route bump.
```bash
npm run gen:operations
```
`scripts/live-check.mjs` talks to a real server over stdio. It reads the token from `LYCHEE_API_TOKEN` or from `LYCHEE_API_TOKEN_FILE` (default `/workspace/tools/lychee/api.token`). It unsets `DATABASE_URL`. It sets `LYCHEE_TOOLSETS=all` for the child process. It prints pass or fail lines only, and it redacts the token. It does not run inside `npm run ci`.
## Config
| Variable | Meaning |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LYCHEE_URL` | Required. Origin only, `http` or `https`, no userinfo, query, hash, or path. Example `http://127.0.0.1:8099`. A URL whose path is already `/api/v2` is rejected. |
| `LYCHEE_API_TOKEN` | Required. The raw token. The client sends `Authorization: Bearer <token>`. A leading `Bearer ` in the variable is stripped. Redacted in logs, errors, and tool results. |
| `LYCHEE_ALLOW_WRITES` | Must be the literal `true` before a confirmed write is sent. |
| `LYCHEE_TOOLSETS` | Comma list, or `all`. Default is the read-only set below. |
| `LYCHEE_PER_MINUTE` | Local request budget. Default 30. Cache hits do not spend it. |
| `LYCHEE_TIMEOUT_MS` | Upstream timeout. Default 20000. |
| `LYCHEE_CACHE_TTL_SEC` | Read cache TTL. Default 60. Writes clear the cache. |
| `LYCHEE_CONTACT` | Contact token in the User-Agent. Must not contain the API token. |
| `MCP_TRANSPORT` | `stdio` or `http`. `--http` overrides. |
| `PORT` | HTTP port. Default 3000. |
Invalid values are reported by field name and issue code. The rejected value is not included.
Every mapped route is called as `/api/v2` plus the PHP path. `Accept` is `application/json`. JSON writes send `Content-Type: application/json`. Multipart upload leaves the boundary to fetch.
## Safety
Default toolsets are reads. Album, photo, tag, and share writes, plus diagnostic maintenance GETs, live on separate toolsets. Those toolsets are off until `LYCHEE_TOOLSETS` includes them or is `all`.
A write on an enabled toolset still needs `confirm` set to the boolean `true` and `LYCHEE_ALLOW_WRITES=true`. The string `"true"` is rejected. Otherwise the tool returns a dry-run preview of method, path, query, and body, and it sends nothing. Omit `confirm` and `blocked` is false. Confirm true with writes disabled and `blocked` is true.
Upload preview does not read `body.file_path` and does not echo the absolute path. A missing file is an input error and sends no bytes.
HTTP 401 is unauthorized. HTTP 400 whose message is `Invalid token` is also unauthorized (Lychee's bad-token response). Other HTTP 400 is invalid input. HTTP 403 is forbidden. HTTP 404 is not found. HTTP 422 is invalid input. HTTP 429 includes Retry-After when the header is present. Other failures are server, network, or timeout. Response bodies are capped at 1,000,000 bytes.
Password fields, including album protection passwords, are redacted in previews and errors. The API token is redacted the same way.
## Tools
Default `LYCHEE_TOOLSETS` publishes 7 tools and 20 read operations. `LYCHEE_TOOLSETS=all` publishes 12 tools and 67 operations. Each tool takes `action` plus optional `params`, `query`, `body`, and, on write tools, `confirm`.
`action` `help` with optional `target` returns the fields for one action. Descriptions list action names only, so the tool list stays small.
| Toolset | Tool | Operations | Default |
| ------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| albums | `lychee_albums` | list, get, tree | yes |
| photos | `lychee_photos` | list, get | yes |
| tags | `lychee_tags` | list, get | yes |
| search | `lychee_search` | init, search | yes |
| sharing | `lychee_sharing` | list, list_all, list_albums | yes |
| users | `lychee_users` | current, rights, list, count | yes |
| settings | `lychee_settings` | init, upload_limits, get, version | yes |
| albums_write | `lychee_albums_write` | create, rename, set_protection, delete, move | no |
| sharing_write | `lychee_sharing_write` | create, edit, delete | no |
| photos_write | `lychee_photos_write` | import, upload, edit, rename, tags, move, copy, star, delete | no |
| maintenance | `lychee_maintenance` | errors, info, space, config, permissions, cleaning, jobs, tree, gensizevariants, missingfilesize, fulltree, countduplicates, searchduplicates, statisticsintegrity, missingpalettes, oldorders, fulfillorders, fulfillprecompute, flushqueue, backfillalbumsizes, recomputetakenatrange, bulkscanfaces, runfaceclustering, destroydismissedfaces, syncfaceembeddings, resetfacescanstatus, purgeorphanfaceembeddings | no |
| tags_write | `lychee_tags_write` | rename, merge, delete | no |
`lychee_albums` `list` is `GET /api/v2/Albums`. `tree` is `GET /api/v2/Album::albums`. `get` is `GET /api/v2/Album::head`.
`lychee_sharing` `list` is `GET /api/v2/Sharing` and requires query `album_id`. `list_all` is `GET /api/v2/Sharing::all`.
`lychee_photos` `list` is `GET /api/v2/Album::photos` with query `album_id`. `get` is `GET /api/v2/Photo/{photo_id}/albums` and only returns album membership. Pass `photo_id` in `params`.
`lychee_search` `search` takes plain text in query `q`. The connector base64-encodes `q` into `terms` and drops `q`. Do not hand-encode. `init` is `GET /api/v2/Search::init`. Pass query `album_id`. An empty string means the root.
`lychee_photos_write` `upload` is multipart `POST /api/v2/Photo`. Pass `body.file_path` plus optional `album_id`, `title`, and `description`. Chunk 1 sends an empty `uuid_name`. Later chunks reuse the `uuid_name` from the previous response. A one-chunk file is only chunk 1. `album_id` may be null for the root. The final tool result includes `expected_id` when Lychee sets it. `import` is `POST /api/v2/Photo::fromUrl`. `tags` needs `photo_ids`, `tags`, and `shall_override`. `delete` needs `photo_ids` and `from_id` (the album the photos are shown from).
`lychee_albums_write` `set_protection` is `POST /api/v2/Album::updateProtectionPolicy`. Album passwords are not a sharing action.
`lychee_settings` `get` is `GET /api/v2/Settings`. A non-admin token can receive 403. `upload_limits` is `GET /api/v2/Gallery::getUploadLimits`. `init` is `GET /api/v2/Gallery::Init`. `version` is `GET /api/v2/Version`.
`lychee_users` reads `GET /api/v2/Auth::user`, `GET /api/v2/Auth::rights`, `GET /api/v2/Users`, and `GET /api/v2/Users::count`.
`lychee_maintenance` is every diagnostic GET under `/Diagnostics` and `/Maintenance::`. `GET /Diagnostics` is action `errors`. Non-GET maintenance routes are excluded.
## What this connector leaves out
Session, login, logout, registration, token reset, WebAuthn, OAuth, gallery unlock, and album unlock stay out. The connector uses a pre-issued API token.
Admin mutations, user management, user groups, settings writes, webhooks, landing editors, moderation, bulk album edit, metrics, statistics, renamer rules, import browsing, and contact forms are excluded.
AI vision, people, faces, and NSFW routes are excluded. So are timeline, map, flow, frame, and other gallery chrome reads.
Binary zip downloads and album track uploads are excluded. `PUT /Sharing` (propagate) is excluded. Album and photo mutations outside the named actions are excluded. Every non-GET maintenance route is excluded.
Each exclusion has a reason longer than 20 characters in `lychee.routes.json`.
## Other servers
There is no public LycheeOrg MCP.
These names are not this connector.
[lycheeverse/lychee](https://github.com/lycheeverse/lychee) is a link checker.
[Lychee-rb2/lychee_mcp](https://github.com/Lychee-rb2/lychee_mcp) is an empty repository.
[ssoma-dev/mcp-server-lychee-redmine](https://github.com/ssoma-dev/mcp-server-lychee-redmine) is a Redmine MCP server.
[delfianto/lychee](https://github.com/delfianto/lychee) is a manga server.
The nearest photo-library servers are [drolosoft/immich-photo-manager](https://github.com/drolosoft/immich-photo-manager) and [barryw/ImmichMCP](https://github.com/barryw/ImmichMCP). Those Immich servers enable writes by default. This connector does not.
## Live check
Last run: **PASS, passed=25, failed=0** against local Lychee 7.12.1 at `http://127.0.0.1:8099` (SQLite, admin token, `DATABASE_URL` unset). Reads, create/rename/upload/tag/delete, write guard, and a bad-token HTTP 400 were exercised. See `LIVE-CHECK.md`.
The parent harness runs `scripts/live-check.mjs` against `http://127.0.0.1:8099` when it chooses to. This package does not start Lychee.
## License
MIT. See `LICENSE`. LycheeOrg/Lychee stays under the Lychee MIT license.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues