macOS 15 SequoiaControl CenterAXSliderMCP-driven

Screen Brightness Control From A Sentence: No IOKit, No brightness CLI, Just An AXSlider

F1 and F2 are for humans. The brightness CLI wraps IOKit. Lunar ships as an app. This page is about the fourth option: type "dim my screen to 40%" into Claude Desktop, and the macos-use MCP server drives the Display brightness AXSlider in Control Center for you. The uncopyable detail is one line: AXSlider sits inside interactiveRolePrefixes at Sources/MCPServer/main.swift:918, which is why the slider surfaces with x, y, w, h and its current AXValue on every traversal.

M
Matthew Diakonov
9 min read
5.0from open source
AXSlider is registered at Sources/MCPServer/main.swift:918 inside interactiveRolePrefixes
Slider AXValue surfaces as inline text via getAXElementText at main.swift:1085-1089
click_and_traverse auto-centers at (x+w/2, y+h/2) per main.swift:1312-1313
pressKey in the SAME tool call steps the slider; the server does not expose drag primitives

Every Other Page About "Screen Brightness Control" Stops Before The Interesting Part

The top Google results for this keyword walk through the obvious surfaces. Apple Support: press F1 and F2, or drag the slider in System Settings. CNET and Lifehacker: the same, with more ads. The product pages for Lunar, MonitorControl, and Brightness Slider: buy our app. What none of them cover is the programmatic path that the arrival of local MCP clients made tractable last year.

Before MCP, driving brightness from a script meant picking one of: the deprecated-on-Apple-Silicon IOKit IODisplaySetFloatParameter call, the private DisplayServices framework (unsigned, breaks under SIP), the brightness CLI (wraps IOKit, same constraints), or brittle AppleScript against System Events. Each was a developer tool. None of them took a sentence.

A macOS-native MCP server driving Control Center's AXSlider is the option the SERP has not caught up with. The rest of this page is the implementation detail that makes it actually work.

Sentence In, Brightness Out

Any MCP client that speaks stdio can call the six tools. The server funnels every request through the same accessibility traversal, and the destination is whatever UI surface holds the slider: Control Center, System Settings, or a third-party app.

The round trip

Claude Desktop
Cursor
Zed
macos-use
Control Center
System Settings
Lunar / app

The One Line That Makes Brightness An Agent Target

If you open Sources/MCPServer/main.swift and jump to line 918, you see the interactive-role registration. It is not a comment block, it is not a long procedure, it is a single array literal. AXSlider shares its row with AXRadioButton, AXPopUpButton, and AXComboBox, and that placement is what turns Control Center's brightness thumb into a visible, targetable element in the compact summary the MCP returns on every tool call.

Sources/MCPServer/main.swift

Removing "AXSlider" from that array would still let the agent access the slider, but it would have to grep the full raw AX dump for the role string and reconstruct the bounding box by hand. With the registration in place, the slider comes out of buildVisibleElementsSection at main.swift:933 as one inline line the LLM can parse with a single grep AXSlider.

How The Agent Reads The Current Brightness Before It Changes Anything

The slider's current position lives in its AXValue attribute. NSSlider stores that as a CFNumber between its min and max; for the Display slider in Control Center the range is 0 to 1. The helper that pulls it sits at main.swift:1085-1089, and the output is emitted as the "text" portion of the visible-elements line in /tmp/macos-use/.

Sources/MCPServer/main.swift

What The Agent Actually Does Between "Dim My Screen" And The Slider Moving

Four steps. The whole sequence uses two of the six MCP tools. No drag primitive is needed because click plus arrow in one call is enough to step an AXSlider.

1

Agent picks macos-use_open_application_and_traverse on com.apple.controlcenter

Control Center is launched (or activated if already running). The server walks the accessibility tree of the panel and writes a compact summary plus a full traversal dump to /tmp/macos-use/<timestamp>_open.txt. The response contains the PID and the file path.

2

Agent greps the traversal for AXSlider and reads its current AXValue

The visible-elements line looks like [AXSlider (slider)] "0.73" x:128 y:96 w:232 h:20 visible. The 0.73 is brightness at 73 percent. If there are multiple sliders (Display plus Sound), the agent picks the one whose label text or neighboring AXStaticText says Display.

3

Agent fires ONE click_and_traverse with element='Display', role='AXSlider', pressKey='Left' or 'Right'

The click lands at the slider's center per main.swift:1312-1313. Swift immediately posts the arrow key inside the same tool handler. The slider's AXValue steps by one tick. The traversal runs again and a diff block comes back showing AXValue "0.73" -> "0.68".

4

Repeat until the delta matches. Every step is one MCP round trip.

The agent computes how many steps remain from the new AXValue, calls again with pressKey='Left' or 'Right', and re-traverses. For 0.73 down to 0.40 at 0.04 per tick, that is about eight steps. Eight small MCP calls, each returning a diff, is cheaper than one long-running drag.

The Stderr Log Of One Real Dim-To-40% Run

Pipe the MCP server into python3 scripts/test_mcp.py and you see this. The two round trips are the open and the click-plus-key.

macos-use MCP server — stderr

The One Tool Call That Moves The Slider

Either form works. The text-match form relies on findElementByText at main.swift:1126 and the role filter at main.swift:1315. The coordinate form copies the x, y, w, h directly out of the traversal text file. Both hit the same code path and both step the slider in one round trip.

mcp-call.json
1 line

AXSlider in the interactiveRolePrefixes array is why an MCP round trip can set your screen brightness. Remove that one string and the agent has to reconstruct the slider's bounding box from the raw AX dump.

Sources/MCPServer/main.swift:918

Screen Brightness Control, Five Ways

The SERP stops at rows 1-3. Row 5 is what this page is about.

FeatureOther optionsmacos-use MCP
Driven by a sentence to an LLMNo (F1/F2, brightness CLI, IOKit, Lunar GUI)Yes. 'Dim my screen to 40%' routes through MCP tools
Uses only public APIsIOKit is public but deprecated on Apple Silicon internal panels; DisplayServices is privateAccessibility API only. AXSlider is public since 10.2.
Step sizeF1/F2: 1/16th per press. Lunar: configurable.One AXSlider tick per arrow key (≈4%). Repeat as needed.
Reads current value before actingF1/F2 are blind. brightness CLI reads through IOKit.Reads AXValue from traversal at main.swift:1085-1089 before deciding delta.
Works with external displaysLunar: yes. IOKit: partially. F1/F2: only if DDC/CI passes through.Whatever Control Center surfaces. For everything else, script Lunar itself via macos-use.
Dependenciesbrightness CLI: IOKit. Lunar: a ~70MB app.A single Swift binary, no helpers on the brightness path.
Line count of the 'why this works' argumentNot applicableOne line. main.swift:918 inside interactiveRolePrefixes.

Sentences An MCP Client Can Route Into Slider Events

Each of these turns into a grep of the traversal plus one or more click_and_traverse calls. Every number the agent reads or writes comes from the slider's AXValue.

Dim to exactly half

'Set my screen brightness to 50%.' The agent reads the current AXValue, computes the delta in slider ticks, and fires click_and_traverse with pressKey='Left' or 'Right' until the AXValue delta matches.

Step with a single call

'Dim by one notch.' One click_and_traverse with element='Display', role='AXSlider', pressKey='Right'. Click focuses the slider, the arrow fires inside the same tool call.

Match ambient light

'Match my screen to the light in this room.' The agent uses open_application_and_traverse on Control Center, checks the AXValue, and decides if auto-brightness already handled it before touching the slider at all.

Scripted bedtime fade

'Drop brightness by 10% every minute until it hits 10%.' Six press_key_and_traverse calls on a timer. Each one grep-checks the AXValue to know whether to step again.

Safe clamp

'Never let me go above 80%.' The agent reads AXValue on every traversal; if the slider reports above 0.80, it fires Left until it is not.

Per-display control

If Lunar is installed, the agent drives Lunar's AXSlider grid in the Lunar window. Same one-line-in-main.swift argument applies across the app's accessibility tree.

The Page In Numbers

These are pulled from the current head of main. If the source moves, the line numbers in this page are the check against what shipped.

0line in main.swift where AXSlider is registered
0MCP tools the agent needs, total
0round trip per slider step (click + pressKey)
0IOKit / DisplayServices calls involved

0 is the line. Every other metric on this page traces back to it.

Try The AXSlider Path On Your Own Machine

Clone mediar-ai/mcp-server-macos-use, build with xcrun swift build, add the binary to Claude Desktop's MCP config, and tell the model to dim your screen. First tool call grabs the PID, second call grep-reads the AXValue, third call steps the slider.

Get macos-use on GitHub

Frequently asked questions

Why does screen brightness control through an MCP work at all without IOKit or the brightness CLI?

Because macOS treats Control Center's Display brightness control as an AXSlider in the accessibility tree, and macos-use registers AXSlider as a first-class interactive role at Sources/MCPServer/main.swift:918 inside the interactiveRolePrefixes array. That array is the exact list the traversal uses to decide which elements get emitted inline as targetable, visible elements with x, y, w, h coordinates. The slider's current brightness (for example, 0.5 for 50%) surfaces as the element's AXValue text via getAXElementText at main.swift:1085-1089. The agent reads the current value from the traversal text file at /tmp/macos-use/, then fires a single click_and_traverse with role='AXSlider' and a pressKey like 'Left' or 'Right' to step. No DisplayServices, no IOKit, no brightness binary, no Lunar. Six MCP tools plus the Accessibility permission.

Why doesn't the MCP just synthesize F1 or F2 the way a keyboard shortcut would?

Because F1 and F2 through a pressKey call become plain virtual-key events, not system-defined media events. The SDK's pressKey implementation at .build/checkouts/MacosUseSDK/Sources/MacosUseSDK/InputController.swift:72-87 uses CGEvent(keyboardEventSource:virtualKey:keyDown:) and posts through .cghidEventTap. That is a standard keyboard event. The actual Mac brightness media keys are NSEventTypeSystemDefined with subtype 8 and keycodes NX_KEYTYPE_BRIGHTNESS_UP (0x90 / 144) and NX_KEYTYPE_BRIGHTNESS_DOWN (0x91 / 145), which this MCP does not emit. The keycode map at InputController.swift:312-313 does map 'f1' and 'f2' to 122 and 120, but those are the virtual keycodes for the physical F-keys, not the media events the OS listens for when it changes brightness. The workable path is to drive the AXSlider in Control Center directly.

What exactly is the 'one line in main.swift' that makes this work?

Sources/MCPServer/main.swift:918 reads: '"AXRadioButton", "AXPopUpButton", "AXComboBox", "AXSlider",' as the second line of the interactiveRolePrefixes array. That array is filtered through isInteractiveRole at main.swift:923-925, and buildVisibleElementsSection at main.swift:933 onward uses the result to decide which accessibility-tree elements get surfaced inline in the compact summary. Without AXSlider in that list, Control Center's brightness slider would be buried several levels deep in the raw tree, and the agent would have to grep for its role string in the full dump. With AXSlider in the list, the summary includes a one-line entry with the slider's bounding box and its current AXValue. That registration is what turns a slider into a first-class target for an LLM.

How does the MCP know what the current brightness percentage is before it changes it?

It reads the AXSlider's AXValue attribute during traversal. The helper getAXElementText at Sources/MCPServer/main.swift:1085-1089 calls AXUIElementCopyAttributeValue with 'AXValue' as CFString. For an NSSlider, that attribute is a floating-point number between 0 and 1 (or the min/max configured on the slider). It lands in the traversal text file at /tmp/macos-use/<timestamp>_<tool>.txt as the 'text' portion of the visible-elements line, formatted as: [AXSlider (slider)] "0.5" x:N y:N w:W h:H visible. The agent greps for 'AXSlider' or for 'Display' nearby, reads the current value, decides the delta, and issues the press_key or click_and_traverse call. If you want to verify, run python3 scripts/test_mcp.py against Control Center while the Display panel is open and grep /tmp/macos-use for 'AXSlider'.

Why click_and_traverse with pressKey instead of two separate calls?

Because click plus arrow press is exactly one atomic sequence and the server is built around minimizing round trips. The tool definition at main.swift:1328-1332 describes click_and_traverse as a single call that 'simulates a click, then optionally types text and/or presses a key'. The instructions block at main.swift:1422-1427 is explicit: 'ALWAYS prefer a single combined call over multiple sequential calls.' For brightness, the first half of the call focuses the slider thumb (making pressKey's arrow actually hit the slider), and the second half presses Right or Left once to step. Doing it as two calls re-traverses the tree twice, doubling latency and burning MCP round trips, and exposes a race where something else could take focus between the click and the press.

How do you actually name the brightness slider in the click call?

Two ways, both supported. The first: pass x, y, width, height copied from the traversal text file. The click lands at (x + width / 2, y + height / 2), the center of the slider's bounding box, per the description at main.swift:1312-1313. The second: pass element='Display' or element='Brightness' as a text match, combined with role='AXSlider' as a role filter (role is explicitly documented as accepting AXSlider at main.swift:1315). findElementByText at main.swift:1126-1149 walks the tree looking for the matching text and returns the center point of its frame. If the slider's accessible label is 'Display Brightness' you can pass that verbatim; if Control Center's label is just 'Display' the role filter is what disambiguates it from the Display text in the section header.

Does this work on external monitors or only the built-in display?

It works on whatever Control Center exposes, which on current macOS Sequoia is the built-in display and any DDC/CI-compatible external display that the OS has taken over control of (a moving target, less reliable than MonitorControl or Lunar for third-party displays). The MCP is not doing anything IOKit-level. It is driving the UI that already exists in Control Center. If your external monitor does not have a brightness control in Control Center on macOS, an MCP call against that slider will not find it in the traversal either. For DisplayPort/HDMI panels that ignore macOS brightness entirely, this approach fails at the OS layer, not at the MCP layer. For those, MonitorControl or Lunar remain the right tools, and you can still script them via macos-use since they run as regular macOS apps with accessibility trees.

Why does the SERP for 'screen brightness control' never mention this?

The top results for that keyword on Google today are Apple Support (covers F1/F2 and the slider in System Settings), CNET / TheVerge / Lifehacker-style walkthroughs (same content, rephrased), and product pages for Lunar / MonitorControl / Brightness Slider. None of them cover programmatic brightness from an LLM, because until MCP landed there was no client that a casual user would have. The programmatic options before MCP were all developer-facing: the brightness CLI (github.com/nriley/brightness, IOKit-based), AppleScript with System Events (fragile), IOKit IODisplaySetFloatParameter with kIODisplayBrightnessKey (deprecated on Apple Silicon for the internal panel), and private DisplayServices APIs (unsigned, breaks under SIP). A macOS-native MCP that drives the Control Center slider turns a one-line English sentence into a deterministic UI action. That crossover is what the SERP has not caught up with.

What happens if Control Center is not already in the menu bar?

Control Center is enabled by default on macOS Ventura, Sonoma, and Sequoia and cannot actually be removed from the menu bar through System Settings. If for some reason its bundle process is not running, the first macos-use_open_application_and_traverse call for com.apple.controlcenter starts it. If the Control Center panel is not yet visible, the agent clicks the menu-bar icon via click_and_traverse (the icon is an AXMenuBarItem in the accessibility tree), which opens the panel. The Display brightness slider appears there, ready to receive the next click. You can also drive System Settings > Displays directly if you prefer a larger slider with a percentage readout; the AXSlider role is the same. Either surface works.

How do I verify this on my own machine in under a minute?

Build the server with xcrun --toolchain com.apple.dt.toolchain.XcodeDefault swift build, then run python3 scripts/test_mcp.py with a sequence: open_application_and_traverse on com.apple.controlcenter, refresh_traversal, and grep /tmp/macos-use/<latest>.txt for 'AXSlider'. You will see one or more lines of the form [AXSlider (slider)] "<value>" x:... y:... w:... h:... visible. The <value> is the current brightness as a float between 0 and 1 (or between 0 and 100 depending on the panel). Then run click_and_traverse with that element's bounds plus pressKey='Right'. Your brightness steps up by one slider tick. Repeat with pressKey='Left' to verify stepping down. The full stderr log goes to the terminal running test_mcp.py, which includes the ActionCoordinator messages confirming the click landed and the key press dispatched.

Does this compete with Lunar or MonitorControl?

No. Lunar and MonitorControl are apps you install and keep running to control external displays over DDC/CI and to smooth Apple Silicon brightness on third-party panels. They own the actual brightness channel. macos-use does not. It drives the macOS accessibility tree, and the thing the accessibility tree exposes for brightness is whatever Control Center shows. If you want LLM-driven brightness on a Dell, install Lunar and then script Lunar itself via macos-use: Lunar has a normal macOS window with an AXSlider per display, and the same one-line-in-main.swift argument applies. You get Lunar's hardware coverage plus the MCP's click-and-press-in-one-call primitive.

macos-useMCP server for native macOS control
© 2026 macos-use. All rights reserved.