Read
MCP tools that only look: they never change a catalog or a photo.
search_photos
Section titled “search_photos”Search photos. Find photos in the open DarkRay catalog with its search syntax: bare words match file name, caption, keywords and place; fields like rating:>=3, label:red, flag:pick, kw:beach, camera:X-T5, lens:35mm, iso:>3200, date:2024-06, type:raw, edited, -edited; content:“red boat” searches by what the photo shows. An empty query lists every photo. Returns ids for the other tools.
Bridge method: photos.search · read-only
| Parameter | Type | Required | Details |
|---|---|---|---|
query |
string | yes | The search, as typed in DarkRay’s search row. |
limit |
integer | Min 1, max 500. Default 50. |
|
offset |
integer | Min 0. Default 0. |
|
catalog |
string | A catalog from list_catalogs; omitted, the one DarkRay shows. |
Python: app.search_photos(…)
from darkray_bridge import DarkRay
with DarkRay.connect() as app: # One page of results, newest first: 50 by default, at most 500. page = app.search_photos("rating:>=4 camera:X-T5", limit=100) print(f"{page.total} matches, {len(page.photos)} on this page") for photo in page.photos: print(photo.id, photo.filename, photo.rating, photo.label)
# The next page starts where this one ended. more = app.search_photos("rating:>=4 camera:X-T5", limit=100, offset=100)get_photos
Section titled “get_photos”Get photo details. Full details of photos by id: file, camera, lens, exposure, size, place, caption, rating, label, flag, keywords, collections, and whether an edit is still being prepared.
Bridge method: photos.get · read-only
| Parameter | Type | Required | Details |
|---|---|---|---|
ids |
array of integer | yes | Photo ids from search_photos (at most 1000). Not empty, at most 1000 items. |
catalog |
string | A catalog from list_catalogs; omitted, the one DarkRay shows. |
Python: app.get_photos(…)
from darkray_bridge import DarkRay
with DarkRay.connect() as app: ids = [photo.id for photo in app.search_photos("flag:pick", limit=20).photos] # Every detail, in the order asked (at most 1,000 photos per call). for photo in app.get_photos(ids): print(photo.filename, photo.camera_model, photo.lens_model, photo.iso, photo.keywords) if photo.missing: print(f" file not found at {photo.path}")get_view
Section titled “get_view”Get the current view. What DarkRay shows now: the mode (grid, loupe, edit…), the folder, collection or search, the selected photos and the current photo.
Bridge method: view.get · read-only
Takes no parameters.
Python: app.get_view(…)
from darkray_bridge import DarkRay
with DarkRay.connect() as app: view = app.get_view() print(view["mode"], view["source"], view["search"])
# Act on what you have selected in DarkRay. if view["selection"]: app.update_photos(view["selection"], rating=4)list_catalogs
Section titled “list_catalogs”List catalogs. The open and recent DarkRay catalogs; tools take one as catalog when it isn’t the one shown.
Bridge method: catalogs.list · read-only
Takes no parameters.
Python: app.list_catalogs(…)
from darkray_bridge import DarkRay
with DarkRay.connect() as app: catalogs = app.list_catalogs() for cat in catalogs: state = "shown" if cat["shown"] else "open" if cat["open"] else "recent" print(f"{cat['name']}: {state}")
# Read from another open catalog without switching DarkRay to it. travel = next(cat["catalog"] for cat in catalogs if cat["name"] == "Travel" and cat["open"]) best = app.search_photos("rating:5", catalog=travel)list_folders
Section titled “list_folders”List folders. The catalog’s folder tree with photo counts.
Bridge method: folders.list · read-only
| Parameter | Type | Required | Details |
|---|---|---|---|
catalog |
string | A catalog from list_catalogs; omitted, the one DarkRay shows. |
Python: app.list_folders(…)
from darkray_bridge import DarkRay
def show(folders: list[dict], depth: int = 0) -> None: """Print a folder tree with its photo counts.""" for folder in folders: note = " (missing)" if folder["missing"] else "" print(f"{' ' * depth}{folder['path']}: {folder['count']}{note}") show(folder["children"], depth + 1)
with DarkRay.connect() as app: show(app.list_folders())list_collections
Section titled “list_collections”List collections. The catalog’s collections with photo counts; the Quick Collection has id 0.
Bridge method: collections.list · read-only
| Parameter | Type | Required | Details |
|---|---|---|---|
catalog |
string | A catalog from list_catalogs; omitted, the one DarkRay shows. |
Python: app.list_collections(…)
from darkray_bridge import DarkRay
with DarkRay.connect() as app: # The Quick Collection comes first, with id 0. for coll in app.list_collections(): kind = "smart" if coll["smart"] else coll["count"] print(coll["id"], coll["set"] or "", coll["name"], kind)
portfolio = next(c["id"] for c in app.list_collections() if c["name"] == "Portfolio") app.show_photos(source={"kind": "collection", "id": portfolio})list_presets
Section titled “list_presets”List presets and looks. Section presets (Color Grading, Curves, Color Adjust…) and creative looks, by name, for apply_preset.
Bridge method: presets.list · read-only
| Parameter | Type | Required | Details |
|---|---|---|---|
section |
string | Only this section’s presets, e.g. color_grade. |
Python: app.list_presets(…)
from darkray_bridge import DarkRay
with DarkRay.connect() as app: # Only Color Grading's presets; leave section out for every section's. found = app.list_presets(section="color_grade") for preset in found["presets"]: print(preset["section"], preset["name"], "(built in)" if preset["builtin"] else "")
# Looks come with every call; apply_preset takes a look by its id. for look in found["looks"]: print(look["group"], look["id"], look["name"])get_preview
Section titled “get_preview”See a photo. See a photo as edited, as a JPEG: the whole photo at a size, or a region at full resolution for judging noise, sharpness, fringing and focus. Says when the image isn’t final yet (an edit still being prepared).
Bridge method: preview.get · read-only
| Parameter | Type | Required | Details |
|---|---|---|---|
id |
integer | yes | A photo id from search_photos. Min 1. |
size |
integer | The long edge in pixels (whole photo). Min 256, max 2048. Default 1024. |
|
region |
object | A box in 0-1 coordinates of the photo as shown, rendered at full resolution (at most 1024 px). Fields: x, y, w, h. |
|
catalog |
string | A catalog from list_catalogs; omitted, the one DarkRay shows. |
Python: app.get_preview(…)
from darkray_bridge import DarkRay
with DarkRay.connect() as app: photo_id = app.search_photos("flag:pick", limit=1).photos[0].id
# The whole photo as edited, 1600 px on its long edge. preview = app.get_preview(photo_id, size=1600) preview.save("whole.jpg")
# The middle of the frame at full resolution, to judge noise and sharpness. preview = app.get_preview(photo_id, region={"x": 0.4, "y": 0.4, "w": 0.2, "h": 0.2}) preview.save("detail.jpg")
# state is "ready" once denoise, if the photo asks for it, is done. if preview.preparation.get("state") != "ready": print("Not final yet, still waiting for:", preview.preparation.get("pending"))get_edits
Section titled “get_edits”Get edit values. A photo’s edit settings by field name, each with its range or choices, and which sections are on. Read it before set_edits.
Bridge method: edits.get · read-only
| Parameter | Type | Required | Details |
|---|---|---|---|
id |
integer | yes | A photo id from search_photos. Min 1. |
catalog |
string | A catalog from list_catalogs; omitted, the one DarkRay shows. |
Python: app.get_edits(…)
from darkray_bridge import DarkRay
with DarkRay.connect() as app: photo_id = app.search_photos("edited camera:X-T5", limit=1).photos[0].id edits = app.get_edits(photo_id)
# Every settable field with its value; number fields carry their range. exposure = edits.fields["exposure_ev"] print(f"Exposure {exposure['value']} EV, from {exposure['min']} to {exposure['max']}")
# Which sections are on, and how many local layers the photo has. off = [name for name, on in edits.sections.items() if not on] print(f"Sections off: {off}; local layers: {edits.local_layers}")find_brackets
Section titled “find_brackets”Find exposure brackets. Group RAW photos into exposure brackets as DarkRay’s HDR merge does, from their EXIF exposures and capture times: each bracket’s ids in capture order with each frame’s EV against the bracket’s middle, and the photos in no bracket. One repeating pattern is looked for across all the photos, so mixed bracket sizes or stray frames can leave photos out; size forces the frames per bracket. Panoramas aren’t detected: group them by capture time and previews.
Bridge method: merge.find_brackets · read-only
| Parameter | Type | Required | Details |
|---|---|---|---|
ids |
array of integer | yes | Photo ids from search_photos (at most 1000). Not empty, at most 1000 items. |
size |
integer | Frames per bracket; left out, detected. Min 2, max 9. | |
catalog |
string | A catalog from list_catalogs; omitted, the one DarkRay shows. |
Python: app.find_brackets(…)
from darkray_bridge import DarkRay
with DarkRay.connect() as app: ids = [photo.id for photo in app.iter_photos("type:raw date:2026-08-21")]
found = app.find_brackets(ids) print(f"{len(found['brackets'])} brackets of {found['size']} frames") for bracket in found["brackets"]: print(bracket["ids"], bracket["evs"]) # each frame's EV against the bracket's middle if found["left_out"]: print("In no bracket:", found["left_out"])
# Force five frames per bracket when detection guesses wrong. found = app.find_brackets(ids, size=5)get_jobs
Section titled “get_jobs”Check jobs. Progress and results of exports, imports and merges.
Bridge method: jobs.list · read-only
| Parameter | Type | Required | Details |
|---|---|---|---|
ids |
array of string |
Python: app.get_jobs(…)
from darkray_bridge import DarkRay
with DarkRay.connect() as app: # Every export, import and merge started through MCP since DarkRay opened. for job in app.get_jobs(): print(job.id, job.kind, job.state, f"{job.done}/{job.total}")
# Or only some, by id. running = [job for job in app.get_jobs(["job-3", "job-4"]) if job.running]