LightPhotos

Your next power user can’t see the screen

Accessibility lets people with disabilities use an app that was designed for a mouse and a pair of eyes. MCP lets an agent use it for a power user who wants a thousand photos done at once. Both ask the app the same questions.

Two users the design leaves out

When I design a screen in LightPhotos, I picture one person. They sit in front of a laptop, they move a mouse, and they look at the photo. Almost every app is designed for that person, and that is correct. Most users are that person.

Accessibility is the work for the people who are not. A blind photographer cannot see the grid, so a screen reader must read it to them. A person with a tremor cannot drag a slider to an exact value, so the keyboard must do it. Accessibility removes the requirement for a particular body. The app still does the same things, and more people can tell it to.

The agentic experience is for a different person that the design also leaves out: the power user. After a wedding, I have 1,200 photos. I want to say “reject every photo where the eyes are closed, rate the sharpest of each burst three stars, and give the rest the warm preset,” and then go and eat dinner. No screen layout makes that fast, because the limit is my hand. One person cannot click 1,200 times without mistakes. An agent can.

So accessibility lets the app do everything for people who cannot use the default input. MCP lets the app do more than one person could do with that input. These two goals look opposite, but they need the same changes to the app. A blind photographer and an agent have one thing in common. Neither of them can see the screen.

The same three questions

A screen reader cannot see pixels. It asks the operating system about the app, through the accessibility tree, and it asks three questions:

LightPhotos answers these questions badly. Its interface is drawn with egui, which paints every button, slider and thumbnail into one GPU frame, 60 times a second. The operating system sees a window full of pixels. egui can describe some widgets through AccessKit, but a photo grid and a develop panel give it very little to say. For a screen reader, most of LightPhotos is one large image. The only way to read it is to capture the screen and guess what the pixels mean.

An agent has the same problem. It can take a screenshot and look at it, and the MCP server has a screenshot tool for that. But a screenshot is a slow, lossy way to learn that photo 412 has three stars. An agent that uses MCP asks the same three questions as a screen reader, and it needs the answers as text. In LightPhotos, they are four tools. list_commands answers “what can I do?” get_state and list_photos answer “what is on the screen?” run_command does one thing, and its reply says what happened, or why it failed.

An app that answers these questions well for a screen reader is most of the way to an app that answers them for an agent. An app that answers them for neither is a set of pixels that only one kind of person can use.

THE SAME EGUI WINDOW LightPhotos ★★☆☆☆ ☆☆☆☆☆ ★★★☆☆ ★★★★★ ★☆☆☆☆ ★★★★☆ One image No photos. No stars. No buttons. ? click (412, 288) photo 1 photo 2 photo 3 photo 4 photo 5 photo 6 WHAT IT GETS window "LightPhotos" image, no name The reader asks for controls. egui gives pixels only. screenshot.png 1440 × 900 px The agent looks at the pixels. Photo 3 has 3 stars, or 4? list_photos {"photo": 3, "rating": 3, …} run_command photo.rate {"stars": 3} CAN IT RATE PHOTO 3? No. It cannot find photo 3. Maybe. It clicks and hopes. Yes. The reply is "ok": true.

The screen reader asks the window for its controls. egui gives one image. The reader cannot find photo 3, so it cannot rate it.

The window is the same for each reader. Only the agent with MCP gets the photos as text. The screen reader does not get this text yet, because egui does not give it to the operating system.
WHO ASKS Keyboard shortcuts Menu bar --drive scripts --control (TCP) lightphotos-mcp WebMCP in the browser ONE LIST OF ACTIONS enum Command 30 ids, each with typed params photo.rate develop.set mask.add …
Every way to tell LightPhotos to do something ends at the same type. A new command is available to all six at once.

What I gave up, and what I got back

In Why Rust, I wrote that I accepted weaker accessibility. Browsers give screen reader support for free. The Rust UI libraries, egui among them, are far behind. In my plan, I wrote that keyboard support is the part of that problem that I can fix myself. So every feature will work without the mouse.

To make every feature work from the keyboard, I had to give every feature a name. That became src/command.rs: one Rust enum, Command, with a variant for each action. Each variant has a fixed id, such as photo.rate, and a params struct, such as {"stars": 4}. The keyboard and the menu bar do not call the editor directly. They make a Command and dispatch it.

When I added the MCP server, most of the work was already done. lightphotos-mcp reads the same enum. list_commands prints each id with an example of its params. run_command parses the JSON into a Command and runs it. I did not write a second API for agents. The agent uses the list of actions that I wrote for the keyboard.

The server runs in two modes. With --folder, it needs no app. It writes ratings, flags, labels and edits into the sidecars next to the photos, and render returns a photo with its edits as a JPEG, so the agent can look at its own work. With --attach, it drives a running app, and screenshot returns the window. In the browser, the web app registers the same tools with WebMCP, so an agent in the browser uses the open tab.

A feature that only the mouse can do

The rule I follow now is this: if a feature has no command, it does not exist for a keyboard user or for an agent. Brush masks showed me where I had broken the rule. A brush stroke was a drag with the mouse, and nothing else. A screen reader user could not paint one. An agent could not paint one either.

To give brush masks to agents, I had to describe a stroke in words: mask.add now takes a list of points. That is also the description an assistive tool needs. When I fixed the feature for the power user, I fixed it for the user with a disability too. The reverse is also true. When you make a feature accessible, you have usually also made it available to agents.

The same is true for errors. A sighted user sees a disabled button and understands. A screen reader user needs the reason in words. An agent needs it in words too, and it also needs the next step. When a LightPhotos tool fails, its reply says what to do next. An unknown id gets “Call list_commands to see every id.” A good error for an agent is a good error for a person.

Where the comparison breaks

The two are not equal. Accessibility is about whether a person can use the app at all. In many countries it is the law. MCP is about speed for people who can already use the app. I am not saying that an MCP server is a substitute for screen reader support. It is not. A blind user of LightPhotos today still gets a window of egui pixels, and MCP does not change that.

An agent can also do damage at a scale a person cannot. One wrong instruction can change 1,200 sidecars. So the agent path has guards that a screen reader does not need. In headless mode, the server checks every photo in a batch before it writes any of them. It refuses a symlink that leads out of the folder. In attached mode, the app requires a token, and the server reads the token only from the environment, so ps does not show it to other users.

What this changes

A feature in LightPhotos is now finished when it has three things: a control on the screen, a Command with a name and params, and state that get_state or list_photos can report. The second and third are what the keyboard, the menu bar and the agent use. They are also the text a screen reader would need. When egui can expose more than pixels, the names and the state will already exist.

I used to think of accessibility as work for a small group of users. Now I think of it as the work that makes the app readable by anything other than a pair of eyes. A person with a disability was the first user who needed that. An agent is the second. The README has the steps to connect an agent. If a feature has no command yet, open an issue.