Skip to content

Computer and browser control

malac-control is a separate binary that exposes the desktop (screenshots, mouse, keyboard) and Chromium-based browsers as MCP tools. malac starts it automatically when it is installed, so the agent can click through a web app, read a page that needs JavaScript or a login, or operate a desktop application.

Because it is a plain MCP server, other agents can use it too, and it can run on another machine: see Controlling a remote machine to test GUIs and websites on a remote desktop.

malac connects to the control service like any other MCP server, trying in this order:

  1. [control] url, if set (and only that).
  2. A running service announced in ~/.malac/control.json, which malac-control serve writes.
  3. A spawned malac-control stdio, found next to the malac binary or on PATH (or [control] command).

The daemon connects the control service once when it starts and shares it between all tabs; a run in its own process (-p, --json) connects its own. The terminal UI’s View menu and the web UI’s home page show whether it is connected and, on macOS, whether it has its permissions.

Its tools are added without a server prefix: computer_screenshot, browser_click and so on. Tools that only read (screenshots, snapshots, listings, waiting) are marked read-only and never need approval; the others follow your permission mode.

To run without it: malac --no-control, or:

[control]
enabled = false
malac-control serve --port 7878 # writes ~/.malac/control.json; malac connects to it
malac-control profiles # list browser profiles

serve listens on 127.0.0.1:7878 by default and requires a token. Without --token (or MALAC_CONTROL_TOKEN), it generates a random one and writes the URL, token and process id to ~/.malac/control.json, readable only by you. malac reads that file and connects; the file is removed when the service stops. Clients send the token as a bearer token or a ?token= query parameter.

A long-lived service is useful on macOS: start it once from a terminal that has the privacy permissions, and the daemon uses it; see Make it stick.

malac control <args> runs malac-control with the same arguments.

To use a service on another machine, set [control] url and token_env; Controlling a remote machine walks through it, including an SSH tunnel for the unencrypted WebSocket.

Each browser profile is a separate Chrome instance with its own user data directory under ~/.malac/browser/<name>, so logins persist between sessions and never mix. A profile called default always exists. Configure more in ~/.malac/control.toml:

[browser.profiles.work]
[browser.profiles.personal]
clone_from = "Profile 1" # copy an existing Chrome profile on first launch
[browser.profiles.debug]
cdp_url = "http://127.0.0.1:9222" # attach to a browser you started with remote debugging
  • A managed profile (no cdp_url) is launched by malac-control with its own data directory. Log in once inside it, and the session cookies stay there.
  • clone_from seeds a managed profile from a system Chrome profile (by directory name, display name or path) on its first launch. Caches are skipped.
  • cdp_url attaches to a browser that is already running with --remote-debugging-port. browser_close only disconnects from it.

Every browser tool takes an optional profile argument (default default), so you can say “use the work profile”. malac-control profiles lists configured profiles and the system browser profiles that can be cloned.

When the service stops, it closes the browsers it launched. Attached browsers keep running.

~/.malac/control.toml (or $MALAC_HOME/control.toml, or --config <path>). Every key is optional.

Key Type Default Meaning
computer.enabled bool true Offer the computer_* tools
computer.max_screenshot_width integer 1366 Wider screenshots are scaled down. At least 320.
browser.enabled bool true Offer the browser_* tools
browser.executable path detected Chrome, Chromium, Brave or Edge binary
browser.headless bool false Run managed browsers without a window
browser.profiles.<name>.clone_from string none System profile to copy on first launch
browser.profiles.<name>.cdp_url string none DevTools endpoint to attach to. Cannot be combined with clone_from.

Profile names may contain letters, digits, - and _. The web UI’s Control service settings edit this file too.

The computer_* tools take a screenshot first; every x/y argument is a pixel position in the latest screenshot of that display, so the agent clicks what it saw. See Control service tools for the full list.

On macOS, desktop control needs Screen Recording (Screen & System Audio Recording on macOS 15 and later) and Accessibility. Browser control needs neither. Without Screen Recording, the tools report the missing permission instead of returning a screenshot of the wallpaper.

# What is granted, and to which app
malac-control permissions
# Show the macOS prompts and open System Settings at what is missing
malac-control permissions --request
# The same report as JSON
malac-control permissions --json
Screen Recording: granted
Accessibility: missing
Granted to: iTerm (/Applications/iTerm.app)

followed by a hint on what to do next. The same report is available to the agent as the computer_permissions tool (read-only; with request, it shows the system prompts), to the terminal UI as View → Request macOS permissions and the control needs permissions status segment, and to the web UI on its home page.

macOS grants these permissions to the responsible app of a process tree: the app the tree started from. It is fixed when the tree starts and survives the daemon detaching itself. So when the daemon starts malac-control:

  • a daemon started from iTerm (by running malac there) runs malac-control as iTerm’s,
  • a daemon started by the VS Code extension runs it as VS Code’s,
  • a process with no app above it, for example started over SSH, gets the permission as the malac-control binary itself.

malac-control permissions names that app under Granted to. Grant both permissions to it in System Settings → Privacy & Security (add it with + if it is not listed), then quit and reopen that app and restart the daemon: malac daemon restart.

Because the daemon can be started by different apps, the grant can seem to come and go. To make it independent of who starts the daemon, run the control service yourself from a terminal that has both permissions:

malac-control serve # in Terminal.app or iTerm, with both permissions granted

It writes ~/.malac/control.json, and the daemon connects to it instead of starting its own malac-control (restart the daemon if it already runs). Keep that terminal open while you work.

Screenshots are images. For models that cannot read images, mark them vision = false and set a vision_model; malac replaces each image with a description written by that model before the request is sent:

vision_model = "lora/vision"
[models."lora/default"]
vision = false

The tutorial Automating a browser sets up a profile, logs in once and runs a scraping task.

malac is created by lobirus and released under the MIT license. About malac