gthumb-mcp
An MCP (Model Context Protocol) server that exposes a gThumb image library to AI agents: catalogs, tags, star ratings, comments, thumbnails and search.
It works directly with gThumb's own data — no database, no duplication:
| Data | Location | Access |
|---|---|---|
| Catalogs & saved searches | ~/.local/share/gthumb/catalogs/ (*.catalog, *.search) |
read + write |
| Tag definitions | ~/.config/gthumb/tags.xml |
read + write |
| Per-tag search snapshots | ~/.local/share/gthumb/catalogs/Tags/*.search |
read |
| Ratings, tags, title, comment | embedded XMP inside the image files | read + write via exiftool |
| Thumbnails | ~/.cache/thumbnails (freedesktop spec) |
read + write |
gThumb picks up everything this server writes on its next refresh — and vice versa.
Requirements
- Linux with gThumb 3.12 (other 3.x versions likely work)
- Python 3.12+ and uv
- exiftool —
sudo apt install libimage-exiftool-perl(everything except the metadata tools works without it)
Install
uv sync
Run
uv run gthumb-mcp # MCP stdio server
Register with an MCP client
opencode (~/.config/opencode/opencode.json):
{
"mcp": {
"gthumb": {
"type": "local",
"command": [
"uv", "--directory", "/absolute/path/to/gthumb-mcp", "run", "gthumb-mcp"
],
"enabled": true
}
}
}
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"gthumb": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/gthumb-mcp", "run", "gthumb-mcp"]
}
}
}
Tools
Catalogs
| Tool | Description |
|---|---|
list_catalogs() |
All catalogs and saved searches with item counts |
get_catalog(name) |
The files stored in a catalog |
create_catalog(name) |
New, empty catalog (supports subfolders, e.g. Random/Trips) |
delete_catalog(name) |
Delete a catalog or saved-search file |
add_to_catalog(name, paths) |
Add images (duplicates skipped) |
remove_from_catalog(name, paths) |
Remove images |
Tags
| Tool | Description |
|---|---|
list_tags() |
All tags known to gThumb |
create_tag(tag) / delete_tag(tag) / rename_tag(old, new) |
Manage tag definitions |
find_images_by_tag(tag, directory?) |
From the cached snapshot, or a live XMP scan of a directory |
Image metadata (embedded XMP, same fields gThumb writes)
| Tool | Description |
|---|---|
get_image_info(path) |
Dimensions, format, camera, date, GPS, title, description, tags, rating |
set_rating(path, 0..5) |
Star rating |
set_tags(path, tags, replace) |
Set (replace=true) or append (replace=false) tags |
set_comment(path, title?, description?, location?) |
Only non-None fields are changed |
Search & view
| Tool | Description |
|---|---|
find_images(directory, name_pattern?, tag?, min_rating?, newer_than?, older_than?, limit) |
Filter by filename glob, tag, rating and modification date |
get_thumbnail(path, size) |
From the shared thumbnail cache (normal 128 px / large 512 px), generated if missing |
get_image(path, max_size) |
Image content, downscaled, for model viewing |
Convenience
| Tool | Description |
|---|---|
open_in_gthumb(path) |
Open a file or directory in the gThumb desktop app |
Example session
User: Show me my catalogs, then the images tagged Beach.
{"tool": "list_catalogs"} → [{"name": "Favorites", "type": "catalog", "file_count": 42}, …] {"tool": "find_images_by_tag", "arguments": {"tag": "Beach"}} → {"source": "snapshot", "count": 118, "files": ["/home/user/Pictures/2024-07/beach_001.jpg", …]}User: Rate the first one 5 stars.
{"tool": "set_rating", "arguments": {"path": "/home/user/Pictures/2024-07/beach_001.jpg", "rating": 5}} → {"path": "/home/user/Pictures/2024-07/beach_001.jpg", "rating": 5}
How it works
Catalogs are parsed and rewritten as XML in exactly gThumb's format. All writes are atomic (temp file +
os.replace), so a crash can't corrupt gThumb's files.Tags on images are read/written as embedded XMP via exiftool, matching what gThumb 3.12 stores in its default store metadata in files mode:
gThumb field XMP field Title XMP-dc:TitleDescription / comment XMP-dc:DescriptionTags XMP-dc:Subject(list)Rating XMP-xmp:Rating(integer 0–5)Location XMP-iptcCore:LocationNote the exiftool list semantics used here: plain
=assignments replace the whole tag list,+=appends, and an empty list withreplace=trueclears it.Thumbnails follow the freedesktop thumbnailer spec: the cache key is the MD5 of the image's
file://URI, and generated thumbnails carry the spec'sThumb::URI/Thumb::MTimeannotations. gThumb shares this cache, so thumbnails are generated at most once, no matter who requests them.find_images_by_tagreads gThumb's cached per-tag snapshot by default (fast, updated whenever gThumb runs a tag search). Pass adirectoryto instead scan the embedded XMP of files live — slower, but reflects the current file contents.If gThumb is running while catalogs change externally, refresh/reopen its library view to see the updates.
Configuration
| Environment variable | Default | Purpose |
|---|---|---|
GTHUMB_CONFIG_DIR |
~/.config/gthumb |
tags.xml location |
GTHUMB_DATA_DIR |
~/.local/share/gthumb |
catalogs location |
XDG_CACHE_HOME |
~/.cache |
thumbnail cache root |
These redirects are mainly useful for testing; no gThumb installation is touched by the test suite.
Development
uv sync
uv run pytest # 13 tests; exiftool-dependent tests skip if exiftool is missing
Project layout:
src/gthumb_mcp/
├── server.py # FastMCP-style server: 19 tools, stdio transport
├── paths.py # gThumb data locations (env-overridable)
├── catalogs.py # .catalog / .search XML parsing and atomic writing
├── tags.py # tags.xml and per-tag search snapshots
├── metadata.py # exiftool wrapper for embedded XMP
├── thumbnails.py # freedesktop thumbnail cache
└── util.py # file:// URI conversion, atomic writes