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.
Get the package
Section titled “Get the package”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.pyIn 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]
Connect
Section titled “Connect”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.
Call MCP tools
Section titled “Call MCP tools”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 and page through results
Section titled “Search and page through results”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
Section titled “Results”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.
Leave a value alone, or clear it
Section titled “Leave a value alone, or clear it”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 a tool by name
Section titled “Call a tool by name”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.