You control the user’s real Microsoft Edge, on the tab they are looking at, with their cookies and sessions. Every action you take is visible to them. Work the way a careful colleague sitting next to them would.
edge_snapshot first. It returns the page as a tree of roles and names with reference IDs:
tab 1459 "Files - Example App" http://localhost:8080/files
page "Files - Example App" url=http://localhost:8080/files
- heading "Files" level=1
- textbox "Search" [e1] value=""
- button "Upload" [e2]
- table "Files"
- row "12080 spec.pdf 2026-09-01" [e4]
- button "Open" [e5]
edge_click {ref: "e5"}, edge_fill {ref: "e1", text: "spec"}. Refs are exact;
text targets (target: "Open") are a fallback when you have no fresh snapshot. Only refs reach
inside an iframe: a ref carries its frame (f31e1), while a text or selector target is looked
up in the main frame alone and comes back target_not_found. If a control you can see in the
snapshot under an iframe line will not resolve, use its ref.stale_snapshot; a removed element fails with stale_ref. Both mean: snapshot again.edge_wait over sleeping: {text: "Saved"}, {url: "/done"}, {load: true}, or
{idle: true} for pages that fetch after load. A timeout tells you what was still loading.edge_console: it holds recent console output, uncaught
exceptions, and any dialogs the bridge answered for you.tab line before actingEvery result carries tab: {id, title, url}. If the user didn’t name a page, confirm from the
first snapshot that you are on the one they meant before you type into it. Your session pins
to that tab afterwards, so a tab the user switches to will not receive your commands.
Snapshots include current values (value="...", checked, selected option). Fill only the
empty fields unless asked to change one. Passwords show as value="••••"; never ask for or
echo the real value.
edge_type sends real key events one character at a time (use it for autocomplete
widgets); edge_fill replaces the value in one step and is faster for plain inputs.<select> needs edge_select, not edge_fill.hidden; edge_upload on their ref or on
the styled button next to them works. Paths must be absolute.edge_tab_close needs an explicit tabId. The bridge will not guess which tab to close.edge_eval runs JavaScript with full page access. Use it for reading state you cannot get
from a snapshot, not as a substitute for clicks (page code sees isTrusted: false events).confirm()/alert() is accepted automatically by default. Set
edge_dialog {policy: "manual"} when the user must decide; the next action then returns
dialog_open until you call edge_dialog with accept or dismiss.Every failure is {success: false, code, error}. Codes you will meet most:
| code | meaning | what to do |
|---|---|---|
stale_snapshot, stale_ref |
refs are from an old page state | snapshot again |
target_not_found |
nothing matched within the timeout | snapshot, pick a ref |
tab_not_found |
the pinned tab is gone | tokenless: the next call re-resolves the active tab; session: expect sticky tab_closed until switch/new |
extension_offline |
Edge is closed or the extension is off | tell the user to open Edge and check the popup |
extension_outdated |
the extension predates the daemon | tell the user to update it |
dialog_open |
a native dialog is waiting | edge_dialog accept or dismiss |
timeout |
the wait condition never held | report what inflight or url shows |
tab_busy |
another call holds the tab lock | backoff 250ms·2ⁿ+jitter ≤4×, then surface |
tab_closed |
session pin points at a closed tab | switch or new to re-pin |
stale_ref |
ref/frame no longer valid | ladder runs automatically; on exhausted_fallback, snapshot fresh |
unknown_session |
bad/expired token (incl. after daemon restart) | client re-mints once; if repeated, session start |
focus_lost |
target never took focus | snapshot, check overlays, coords click |
focus_unverifiable |
frame context unresolvable | retry, coords-only click, or abort |
focus_stolen |
focus moved mid-type | resume with remaining |
deadline_exceeded |
extension passed deadlineMs |
retry (locks/deadlines reset), split long writes |
write_mismatch |
readback differs after retry | report expected vs readback, stop |
exhausted_fallback |
ref→text→scan→coords all failed | read tried, snapshot, new approach |
history_failed |
history query failed in the extension | report error, retry once |
group_failed |
tab-group call failed in the extension | report error, check tab IDs |
pip install edge-agent-bridge
edge-bridge setup # registers the MCP server with the agent clients it finds
edge-bridge status # daemon, extension, and pairing state
The extension comes from the Edge Add-ons store (“Edge Agent Bridge”); developers can load
edge-bridge extension path unpacked instead.
CLI invocations are stateless, so each agent holds a daemon-side session pinned to one tab:
edge-bridge session start # prints sessionToken=<uuid>
export EDGE_BRIDGE_SESSION=<uuid> # or pass --session <uuid> per call
edge-bridge switch 1459 # pin this session to your tab
edge-bridge session status # shows pin state
edge-bridge session stop # drop it server-side
Forgetting --tab is harmless inside a session: the daemon injects your pinned tab. An explicit --tab is a one-shot override and never changes the pin. Only switch and new re-pin. MCP clients get an implicit session per process automatically. If your tab closes, the session goes sticky tab_closed — every tab-scoped call fails until switch/new. There is no shared default session: two agents MUST use distinct tokens or they share one pin.
Multi-agent etiquette: own tab each. Per-tab locks serialize writers as defense-in-depth, not as an excuse to share a tab.
edge_history_search {text, maxResults?, startTime?, endTime?} searches titles and URLs
(startTime/endTime are ms since epoch); edge_history_delete {url} removes one entry.
edge_group_list {windowId?} lists tab groups; edge_group_move {tabIds, groupId?, title?, color?}
moves tabs into an existing group, or a new one when groupId is omitted;
edge_group_ungroup {tabIds} removes tabs from their groups. CLI mirrors:
edge-bridge history search "query", history delete <url>, group list [--window-id N],
group move <tab ids> [--group-id N] [--title T] [--color C], group ungroup <tab ids>.
match, write_mismatch, focus_stolenedge_fill/edge_type verify every write. Results carry written (expected full value), readback (value read back after 100ms), and match:
match: true — written value confirmed.match: false — the client already retried once with fill semantics; a second mismatch returns write_mismatch with {expected, readback, attempts: 2}. Report both values and STOP; do not continue silently.match: "unknown" — the extension predates readback (check edge-bridge status: old extension_version means expected degradation, new version means broken readback). The client warns once and continues unverified.Focus is asserted before every write (≤3 tries) and rechecked after the 1st char and every 8 chars while typing:
focus_lost — target never took focus. Snapshot, check overlays, try a coords click.focus_unverifiable — the target frame vanished mid-write. Retry, coords-only click, or abort.focus_stolen — the human (or page) moved focus mid-type. Resume by typing remaining (exactly typedSoFar chars already landed).tab_busyRef-taking actions (click, dblclick, rightclick, hover, fill, type with target, select, check-radio, upload, drag) automatically retry ref → text/selector → fresh elements scan → coords from the best match (exact, then case-insensitive, then substring text match), with at most 2 extra scans per call. Explicit --x/--y skips the ladder; --no-fallback disables it. Total failure returns exhausted_fallback with tried: [{step, outcome}] — read it, snapshot fresh, try a new approach.
tab_busy means another writer holds your tab’s lock (10s acquire). Back off 250ms·2ⁿ with jitter, ≤4 retries, then surface or use another tab.