Skip to content

Read

MCP tools that only look: they never change a catalog or a photo.

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 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 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. 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. 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. 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 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"])

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 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 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)

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]