Skip to main content
Glama
Intrect-io

lyra-browser

Official
by Intrect-io

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
HERMES_HOMENohermes: data goes to `$HERMES_HOME/browser`.~/.hermes
VEGA_DATA_DIRNovega: data goes to `$VEGA_DATA_DIR/browser`.
LYRA_BROWSER_GUARDNoWhere the navigation guard stands: `route` (Playwright's route) or `cdp` (a second DevTools connection: every redirect hop judged, cache on, DataDome lets the browser in, a loopback debugging port, `guard_lost` if the guard fails). See Guard backends.route
LYRA_BROWSER_PROXYNoRoute the browser through a proxy, e.g. `socks5://100.x.y.z:1080`. For UAT runs that must not share the operator's egress IP, since per-IP rate limits, quotas and IP-based analytics exclusion all key on it. Unset means a direct connection.
LYRA_BROWSER_CLIENTNo`vega`, `hermes` or `generic` (also `--client`). Picks default paths and window mode only, never permissions. Detected from `VEGA_DATA_DIR` → vega, `HERMES_HOME` → hermes.detected
LYRA_BROWSER_DRIVERNoWhich Playwright to launch with: `auto` takes `patchright` when installed and falls back to `playwright`; either name pins it. `open_browser` reports the one in use as `driver`.auto
LYRA_BROWSER_CHANNELNoForce one channel (`chrome`/`msedge`). Unset = try chrome→msedge→bundled.
LYRA_BROWSER_DATA_DIRNoBase data dir (profile + audit). Overrides everything.
LYRA_BROWSER_HEADLESSNoRun without a visible window (see Headless mode). Unset: headless for hermes, and on Linux with no `DISPLAY`/`WAYLAND_DISPLAY`; otherwise a window.auto
LYRA_BROWSER_VIEWPORTNo`WIDTHxHEIGHT` of the page (`390x844` for a phone layout). Headless uses it as the emulated viewport; headful as the window size. Anything unreadable falls back to the default.1280x800
LYRA_BROWSER_GRANT_TTLNoSeconds a grant stays usable. Single-use ones ignore this.600
LYRA_BROWSER_CAPTURE_DIRNoWhere `screenshot`/`read_image` write PNGs. Default: vega `$VEGA_DATA_DIR/uploads/browser` (where VEGA attaches images), hermes `$HERMES_HOME/cache/browser` (where Hermes' vision may read), else `<data_dir>/captures`.
LYRA_BROWSER_ENFORCEMENTNo`observe` records what it would have blocked without blocking: navigations go through, and an undeclared download is saved (`observed: true`, audited `would_block`) instead of cancelled.enforce
LYRA_BROWSER_CAPTURE_KEEPNoCaptures kept on disk; oldest are deleted first. `0` keeps all.200
LYRA_BROWSER_DOWNLOAD_DIRNoWhere declared downloads are saved. Default `<data_dir>/downloads`.
LYRA_BROWSER_ALLOW_BUNDLEDNoAllow bundled-Chromium fallback (only exists after `playwright install`).true
LYRA_BROWSER_DOWNLOAD_KEEPNoSaved downloads kept; oldest are deleted first, and only files this server saved (tracked in a ledger in the dir). `0` keeps all.200
LYRA_BROWSER_RELEASE_GRACENoSeconds before a finished action's unused single-use scope is reclaimed.2
LYRA_BROWSER_CONSENT_CHANNELNo`auto` asks the user over MCP and falls back to `confirm=true` only on a client that cannot be asked; `elicit` pins asking and denies otherwise; `legacy` always takes the model's word; `off` does not ask.auto
LYRA_BROWSER_CONSENT_TIMEOUTNoSeconds a prompt waits for an answer before it counts as a denial (`timed out waiting for the user`). The tool call is held that long, so for an unattended Hermes gateway set it lower — e.g. `60`.300
LYRA_BROWSER_TRUSTED_ORIGINSNoSites pre-approved for `NAVIGATE`/`INTERACT`, comma- or whitespace-separated: `https://host[:port]`, a bare `host` (https only), or `*.example.com` (https subdomains, not the apex). Never covers `SUBMIT`/`UPLOAD`/`PUBLISH`/`DOWNLOAD`. See Trusted origins.
LYRA_BROWSER_DOWNLOAD_TIMEOUTNoSeconds a declared download may take to arrive once it has started before it is cancelled (`download_failed`, `timeout`).120
LYRA_BROWSER_REQUIRE_APPROVALNoAsk before risky actions. `false` sets the consent channel to `off`.true
LYRA_BROWSER_DOWNLOAD_MAX_BYTESNoLargest file a download may be (200 MiB); a bigger one is cancelled and answered `download_failed` (`too_large`).209715200
LYRA_BROWSER_OWNER_IDLE_TIMEOUTNoSeconds a session may hold the browser without using it. Handing it on closes the window.900
LYRA_BROWSER_TRUSTED_SEND_ORIGINSNoSites where *sending* is also pre-approved — `SUBMIT` and `UPLOAD` — for acceptance runs against your own product. Same entry forms, **separate list**: a site in `TRUSTED_ORIGINS` is not send-trusted by being there. Never `PUBLISH` or `DOWNLOAD`; each approval stays single-use, and it steps aside during a takeover. Audited as `consent_channel=trusted_send`.

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": true
}
logging
{}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
extensions
{
  "io.modelcontextprotocol/ui": {}
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
open_browserA

Launch (or focus) the visible browser window the user shares with you.

If no browser is installed this returns a browser_unavailable envelope (relay it so the user can install Chrome) rather than failing.

close_browserC

Close the shared window, discarding every approval it earned.

This is how a session finishes: the window goes away, the grants go with it, and the next session may claim the browser. Without it the refusal another session receives would name a wait that never ends.

navigateA

Navigate the shared window to a URL.

Going to a different site asks for permission to be on that site. Moving within the current one does not. A URL with no host — file:, data:, javascript: — is never "the same site" and is always asked, every time, because one approval there would cover every local file or every document the agent can author.

ok means the navigation happened, not that the page is good: read http_status (404, 500 ...) and content_type (application/pdf ...) of the page that answered. Both are null when no HTTP response was involved (about:blank, data:). wait_until is how much of the load to wait for: domcontentloaded (default), load, commit or networkidle — which never settles on a page that keeps polling. A page that draws itself after it has loaded needs wait_for. A URL that turns out to be a file download does not move the tab: with download=true — which asks for permission to write a file — it is saved and the reply carries download (filename, path, bytes, url_origin); without it the download is cancelled and answered download_blocked, so repeat the call with download=true. The permission pays for one file and ends with the call.

go_backB

Go back one entry in history.

Where "back" leads is not known until it happens, so this asks only to interact with the current site; landing somewhere else is judged then. Reports http_status and content_type like navigate — both null when the browser restored the page without asking the server.

reload_pageC

Reload the current page. Reports http_status and content_type like navigate.

clickA

Click the first element matching selector (CSS or Playwright text=).

Set submits=true when the control sends a form or completes a purchase — that asks for the permission it needs. If the page submits anyway without you declaring it, the request is stopped, so declare it when you mean it. reason is shown to the user when they are asked.

Fails fast instead of hanging. A selector that matches nothing answers not_found, and a match that cannot be used hidden or disabled — each after at most a few seconds' wait for a page that is still building or enabling its controls, and before anyone is asked. Once clicking, timeout_ms (default 10000, max 30000) bounds the wait, and a failure answers timeout (the click may have landed — look at the page before clicking again), element_not_actionable (covered, detached) or page_closed.

The reply says how many elements matched (matches; the first is clicked) and what was hit (clicked: tag, role, name), so a wrong pick is visible.

If the click makes the page navigate and the browser refuses it — no approval covers the destination, or a form was sent without submits=true — the reply is blocked_by_policy (with url, where the tab still stands, and redirected_to when the site redirected the browser elsewhere) and the tab has not moved: navigate to the destination to be asked for it. A click that opens a tab answers new_tab: true and tab_count; the new tab is the one every later call reads and acts on (tabs goes back).

Set download=true when the click is meant to save a file: that asks for permission to write one, and the reply carries download (filename, path, bytes, url_origin) once it is saved. A click that starts a download without it is cancelled and answered download_blocked — nothing is saved, so repeat the click with download=true. The permission pays for one file and ends with the call.

A native dialog the page raised is answered at once and listed under dialogs in the reply; handle_dialog chooses the answer beforehand.

type_textA

Fill selector with value. The value is redacted from the audit log.

Set submit=true to press Enter afterwards, which also asks for permission to send the form. Note that some pages submit on typing alone; that is stopped unless you declared it, and answered blocked_by_policy (the tab has not moved; navigate to the destination to be asked for it).

Fails fast like click: not_found, hidden or disabled before anyone is asked, then timeout, element_not_actionable (read-only, not a text field, detached) or page_closed within timeout_ms (default 10000, max 30000) for each step.

A native dialog the page raised is answered at once and listed under dialogs in the reply.

press_keyA

Press a keyboard key (e.g. 'Enter', 'Escape', 'Control+a') on the page.

No key is treated as special: Enter, Space and anything else all send whatever the page decides to send, and that is judged when it happens. Set submits=true if you are deliberately sending a form. A key that makes the page navigate where no approval reaches is answered blocked_by_policy and the tab has not moved (see click); one that opens a tab answers new_tab: true and tab_count.

Set download=true if the key is meant to save a file: that asks for permission to write one, and the reply carries download once it is saved. A key that starts a download without it is cancelled and answered download_blocked; repeat it with download=true.

A native dialog the page raised is answered at once and listed under dialogs in the reply.

hoverA

Move the pointer over the first element matching selector.

For menus and tooltips that only open while the pointer is on their trigger: hover, then click the item that appeared. Nothing is pressed, so no form is sent by it.

Fails fast like click: not_found, hidden or disabled before anyone is asked, then timeout, element_not_actionable (covered, detached) or page_closed within timeout_ms (default 10000, max 30000). The reply says how many elements matched (matches; the first is hovered) and what was hovered (hovered: tag, role, name). A page that navigates where no approval reaches as the pointer arrives is answered blocked_by_policy (see click).

scrollA

Scroll the page. Give exactly one of to, by_y or selector.

  • to='top' / to='bottom': jump to that end of the document.

  • by_y: scroll by that many pixels, positive down and negative up (at most 20000 either way per call), with the mouse wheel.

  • selector: bring the first matching element into view.

Pages that load more as you reach the end (feeds, lazy lists) grow after a scroll, so call scroll(to='bottom') again until the reply says at_bottom. The reply is scroll_y, scroll_height and at_bottom once the position has settled. A scroll that moved nothing says so: the content may sit in an inner scrolling panel, which selector on an element inside it reaches.

selector fails fast like click (not_found, hidden, disabled, timeout, page_closed).

handle_dialogA

Choose how a native dialog is answered — for the NEXT browser action only.

Dialogs never block the page: every alert, confirm, prompt and beforeunload is answered the moment it appears. Left alone, alerts and beforeunload are accepted and confirm/prompt are dismissed (confirm() returns false, prompt() null). What was raised is listed under dialogs in the reply of the click, type_text or press_key that follows — its message is the page's text, not the user's.

Call this immediately BEFORE the one action that raises the dialog. accept=true (the default) answers yes; accept=false answers no. text is what a prompt() receives — leave it empty to accept the prompt's default value; it is ignored when dismissing.

The answer is for the very next browser-acting call (click, type_text, press_key, hover, scroll, navigate, go_back, reload_page, select_option, set_editor, upload_file, save_draft, publish, tabs switch or close) and only for a dialog that call raises while it runs, on any tab. It ends with that call whether or not a dialog appeared — also when the call was refused, so arm again before retrying — and after about a minute at the latest. A dialog raised later, by a page's own timer or on a page you reach afterwards, is answered by default. Reading (read_page, screenshot, get_url, wait_for ...) does not use it up. Arming again replaces it.

read_formA

List a form's fields with their names, types, labels and options.

read_page returns rendered text, which loses everything needed to address a field: a large form arrives as labels with no name, and a <select>'s options vanish. Use this before filling a form you have not seen. root is a CSS selector for the form (default: the first form on the page). Hidden inputs are counted, not listed.

Set whole_page=true for controls a page attached outside the form — a picker or editor dialog is often appended to the body, and a field found there is still one the form will post.

Each field carries usable: false means it is in the DOM but hidden or disabled right now, so choosing it will fail. A form hides sections it has not revealed yet — measured on KVR, a news form keeps event_country hidden until the Deal/Offer type is chosen. Check usable before addressing a field rather than discovering it through a timeout.

select_optionA

Choose an option in a <select>, by value, label or index.

Give exactly one of the three. A dropdown is not a click target: this sets the value and fires change, which is what a page listens for. Set submits=true when the choice sends the form (an onchange that posts) — that asks for the permission it needs.

set_editorA

Write into a rich-text editor — the body a form actually posts.

For CKEditor, TinyMCE and similar the visible editor is an iframe whose body is editable, and the <textarea> the form submits is hidden and empty: type_text on that textarea does nothing. Leave selector empty to use the first editor on the page, or pass the editor's own iframe or [contenteditable] element.

html=true writes markup (links, paragraphs, images) and syncs the editor's own data when it is a CKEditor instance; otherwise the text is written verbatim. Set submits=true if the write sends the form.

upload_fileA

Attach one or more local files to a file picker.

This hands a file on this machine to a page, which is why it asks for its own permission, once per call. selector is the input[type=file]; a styled picker often keeps it hidden, and that is fine — a hidden input still accepts files. Missing paths are reported before anything is attached, so a gated call never leaves a picker half-filled.

read_draftA

Collect what a form is about to publish, for a human to approve.

Approving an item means reading the whole of it, and on a real form that means looking in several places at once: the visible fields, the rich-text body (a separate document read_page never shows), the attachment slots, and whether the item is currently a draft or already public. This gathers all of it into one answer so the approval request can carry the actual content rather than a description of it.

Nothing is changed or sent — this is a read, so it needs no approval. Fields carry usable; one that is false is hidden or disabled in the page right now and must be revealed before it can be set.

save_draftA

Save the item without showing it to anyone — the safe way to send a form.

A site with no separate "save draft" button means every form POST is the one that could publish, so the difference has to be read off the page rather than chosen by the caller. This sends the form only after confirming the publish control is still on its private setting, and refuses if it is not — so it cannot be used to publish, and a page left armed by anything else will not slip out through it.

Measured on KVR: a new news item opens with is_draft=1 and posting the form then saves it privately.

submit names the control that sends the form — read_form reports it in submitText (Submit on a news item, Add on a product). This asks for SUBMIT but never for PUBLISH: saving is an ordinary request, and keeping the two apart is what lets an operator write and keep an item without ever approving a release.

Use publish instead when the item should go out to an audience.

publishA

Make a draft public — the one action that sends content to an audience.

Kept apart from every other tool so that writing a draft can never publish it: this asks for PUBLISH, which is single-use and implied by nothing, and it is the only tool that buys it.

A site splits this across two controls and means neither of them alone. Measured on KVR: the publish radio (input[name=is_draft][value='0'] on a news item, input[name=is_live][value='1'] on a product) only arms the form — the item travels on the form's own Submit. Switching the radio and stopping would leave the page primed to publish on someone else's next click, which is worse than not switching it at all. So give both:

  • selector — the control that carries the item live, from read_form's drafts list.

  • submit — the control that sends the form. Required: read_form reports the candidates in submitText, and on KVR they are one button (Submit on a news item, Add on a product). This is the request that actually publicises the item, so nothing else in this tool surface may send a form whose publish control is set.

Call this only when a human has approved the exact item. Show them what will go public and get their answer first; the approval prompt this raises is the second gate, not the first. Afterwards read_form reads back the state the page actually reached.

get_urlA

Return the current URL and page title, and how many tabs are open (tab_count).

read_pageA

Read the page as text, or as a map of what can be acted on.

mode="text" (default) returns the rendered text (innerText), not HTML. links=true adds the page's visible links as {text, href} — absolute, de-duplicated, at most 100 (tree lines carry their own href).

mode="tree" returns one line per visible thing you can act on — links, buttons, inputs, selects, checkboxes, tabs, menu items — plus headings::

aria-ref=e12 link "Pricing" -> /pricing
aria-ref=e15 button "Save"
aria-ref=e16 checkbox "Remember me" [checked]

Pass the aria-ref=... token exactly as written as the selector of click or type_text. It reaches what a text selector cannot name: icon-only links, several links with the same text, elements inside open shadow roots and iframes. A ref is good for the page it was read from and only while its element stays; a region read (selector) replaces the refs of the read before it. When a ref is rejected or matches nothing, read the tree again. Elements on screen come first (in_viewport counts them). Input values are never shown. refs: false means this Playwright predates aria refs: lines then carry none, so address elements with role=link[name="Pricing"] or text= selectors.

selector limits either mode to one region (first match); it returns not_found at once when nothing matches. offset and max_chars page through long output: total_chars is its full length, truncated says more follows, next_offset is where to continue. Tree pages hold whole lines, so a ref is never cut in half. Every call reads the page as it is now, so pages read while it changes or scrolls may not line up.

screenshotA

Capture the page as a PNG file and return its path.

Use it to see the page — layout, images, charts, anything read_page cannot put into words. The file is what you look at; inline=true adds base64 only for a client without access to this machine's filesystem.

read_imageA

Capture one element — an img, canvas, svg, a chart, a map — as a PNG file and return its path.

This is how you look at a picture on the page: it captures the pixels the element renders, so it works for anything drawn, not only img. Nothing is fetched — no new request leaves the browser. Returns not_found when selector matches nothing.

tabsA

List the browser's tabs, switch to one, or close one.

  • action="list" — every open tab with its index, URL, title and whether it is the active one (the one you read and act on).

  • action="switch" — make tab index the active one, and bring it to the front so the user sees what you see.

  • action="close" — close tab index. Closing the active tab returns you to the tab that opened it, else the newest one; closing the last tab leaves a blank one.

Indexes shift whenever a tab opens or closes, so list again before using one. Tabs a page opens are followed automatically, and a popup that closes itself (a sign-in window) returns you to where you were — this is for when you have to choose. Switching to or closing a tab asks for permission to use that tab's site, like any other action there; reason is shown to the user when they are asked.

wait_forA

Wait until the page is ready, instead of sleeping or clicking to pass time.

Give exactly one condition:

  • text: the phrase is in the page's visible text — what read_page returns. Whitespace does not matter, case does. state="hidden" waits for it to be gone instead (a "Loading..." message).

  • selector: an element matching it (CSS or Playwright text=) reaches state: visible (default), hidden, attached or detached.

  • url: the tab's URL matches. A glob over the whole URL, so use **/checkout** or https://site.example/cart*, not checkout.

  • load_state: load, domcontentloaded or networkidle.

Waits up to timeout_ms (default 10000, never more than 30000) and returns {"status": "ok", "waited_ms"}. Running out of time is an answer, not a failure: {"status": "timeout", "waited_ms", "last_seen"}, where last_seen is a short excerpt of the page text (the URL for a url wait) so you can see why. Bad arguments return error.

Use it after navigate or an action when the page draws content late: a page that renders 300ms after load looks empty to read_page until this returns. It only reads, so it needs no approval and works while the user has taken over.

list_downloadsA

List the files this session has downloaded, oldest first.

Each entry is filename, path, bytes and url_origin (the site the file came from), the same as the download a call returned when it saved one. A download that was blocked or failed left no file and is not listed. This only reads: it asks nobody, works during a takeover and does not open the browser.

highlight_elementA

Outline an element in the shared window so the user can see what you mean.

Requires an attended (headful) session — in headless mode there is no one to show it to, so this returns unattended.

ask_user_to_doA

Ask the user to perform a step you must not do yourself.

Use for credentials, CAPTCHAs, 2FA, or payment confirmation. Highlights the relevant element when selector is given, then returns an awaiting_user envelope. Relay the instruction, then poll the page (read_page / get_url) to detect completion.

Requires an attended (headful) session — in headless mode there is no user to ask, so this returns unattended rather than making you wait.

request_takeoverB

Hand control to the user. Agent mutations are blocked until they hand back.

Requires an attended (headful) session. In headless mode the takeover is refused and no state changes: blocking every mutation while waiting for a user who cannot see the window would deadlock the run.

resume_after_takeoverB

Resume agent control after the user has handed the session back.

Releasing a takeover goes through the consent channel: an agent that can clear its own lock can ignore the lock. On a client without elicitation this still succeeds — the release is recorded as the model's own assertion, which is what consent_channel=legacy in the audit means.

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

B3.4/5.0

Scored across 29 tools

Disambiguation4/5

Most tools have clearly distinct purposes within a browser-automation domain. Some potential for confusion exists between read_page (text/tree reading) and read_form (form field listing), or between navigate/go_back/reload_page. However, the detailed descriptions strongly clarify when to use each, keeping overlap manageable.

Naming Consistency3/5

Mix of verb_noun (open_browser, close_browser, read_page, read_form, read_image) and verb-only (click, type_text, navigate, scroll, hover) patterns. The conventions are still readable and the verbs are clear, but there is no single predictable pattern across all 29 tools.

Tool Count2/5

29 tools is at the high end for a browser automation server, especially since many are fine-grained actions (click, type_text, press_key, hover, scroll) that could be combined into a more general 'interact' tool. The count feels heavy for the domain, though each tool does have a specific role.

Completeness4/5

Covers a wide range of browser automation needs: navigation, reading, interaction, downloads, forms, tabs, dialogs, and session control. A few gaps remain (e.g., no explicit tool for executing JavaScript or managing cookies/local storage, no direct window resizing), but these are minor for most agent workflows.

Maintenance

ActivityMaintained
ResponsivenessNo issues