Skip to content

The Python client

The darkray_bridge package lets a Python script do everything an assistant can, on as many photos as you like. It has one method per MCP tool, with the tool’s name and parameters.

darkray_bridge is a folder in DarkRay’s source folder, beside app. It needs only the Python standard library, so any Python 3.10 or later can run it; your script doesn’t need DarkRay’s own environment.

Put the folder that holds darkray_bridge on Python’s path, for example in PowerShell:

$env:PYTHONPATH = "Q:\dev\liminal"
python my_script.py

In the Command Prompt it is set PYTHONPATH=Q:\dev\liminal; on macOS and Linux, export PYTHONPATH=~/dev/darkray. [CHECK: where an installed DarkRay will place darkray_bridge; SPEC/MCP_API.md plans a scripting/ folder beside the app, not built yet]

DarkRay must be running with MCP on (see Turn it on). DarkRay.connect() finds it through ~/.darkray/bridge.json and opens a connection:

from darkray_bridge import DarkRay
# Find the running DarkRay through ~/.darkray/bridge.json and say hello.
# The with block closes the connection when it ends.
with DarkRay.connect(timeout=5) as app:
print(f"Connected to DarkRay {app.app_version}")
print(f"{app.search_photos('').total} photos in the catalog shown")

timeout is how many seconds to wait for the connection (10 by default). Two keyword options are rarely needed: client, the name DarkRay shows for your script on the Console’s MCP card, and bridge_path, another discovery file to read.

Use the with form so the connection closes even when the script fails. Without it, call close() yourself:

from darkray_bridge import DarkRay
# Without a with block, close the connection yourself when you're done.
app = DarkRay.connect()
try:
page = app.search_photos("rating:5")
print(f"{page.total} five-star photos")
finally:
app.close()

A connection belongs to one thread. Calls block until DarkRay answers.

Each method is named after an MCP tool and takes its parameters, so the MCP tool reference is also the client’s reference; every entry there has a Python example. Changes go to the catalog DarkRay shows; reading methods also take a catalog from list_catalogs to read another open one.

search_photos returns one page, 50 photos by default and 500 at most:

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)

iter_photos follows every page for you, 500 at a time:

from darkray_bridge import DarkRay
with DarkRay.connect() as app:
# iter_photos follows every page of a search for you, 500 photos at a time.
ids = [photo.id for photo in app.iter_photos("kw:wedding type:raw -edited")]
print(f"{len(ids)} unedited wedding RAWs")

Methods that take a list of photos accept up to 1,000 ids per call. For more, split the list.

Results come back as small Python objects (dataclasses) where the shape is fixed, and as plain dictionaries and lists elsewhere:

Method Returns
search_photos SearchPage: total and photos, a list of PhotoBrief
iter_photos PhotoBriefs, one at a time: id, filename, date_taken, rating, label, flag, is_raw, edited, copy_name
get_photos A list of Photo: a PhotoBrief plus path, missing, camera, lens and exposure, size, place, GPS, caption, keywords, collections, in_quick_collection, copy_of_id, preparation
get_preview Preview: see Jobs and previews
get_edits EditFields: see Edit field names
import_photos, merge_photos, export_photos, get_jobs, cancel_job Job: see Jobs and previews
update_photos, apply_preset, copy_edits The ids of the photos that changed
update_collection The collection’s id
open_catalog The catalog’s key, its path
show_photos Nothing
The rest The tool’s result as a dictionary or list, as the reference shows

An object ignores fields it doesn’t know, so a script keeps working with a newer DarkRay.

A parameter you leave out isn’t sent, and DarkRay leaves that value as it is. None is sent and clears the value: a color label, a flag or a caption.

from darkray_bridge import DarkRay
with DarkRay.connect() as app:
ids = [photo.id for photo in app.iter_photos("kw:beach date:2026-07")]
# One call is one undoable step in DarkRay.
app.update_photos(ids, rating=3, label="blue", add_keywords=["summer", "Portugal"])
# Mark the best one, and caption it.
app.update_photos(ids[:1], flag="pick", caption="Praia da Marinha at sunrise")
# None clears a label, flag or caption; a parameter left out stays as it is.
app.update_photos(ids[1:], label=None, remove_keywords=["summer"])

To clear a rating, set rating=0. Leaving a parameter out is the same as passing darkray_bridge.UNSET, which is useful when your own function passes values through.

call() sends an MCP tool by name and returns DarkRay’s JSON as it comes, without turning it into objects:

from darkray_bridge import DarkRay
# call() sends an MCP tool by name and returns DarkRay's JSON as it comes,
# without turning it into dataclasses. Parameters are the tool's own.
with DarkRay.connect() as app:
raw = app.call("search_photos", query="flag:pick", limit=10)
print(raw["total"], [photo["id"] for photo in raw["photos"]])

It knows only the MCP tools in your copy of darkray_bridge; another name raises KeyError.

Next: Jobs and previews, then Errors and timeouts.