The agent had a list. I asked it to pick an item. It refused.
Element not found
Refresh. Same.
So I opened DevTools and pasted in:
document.querySelector('select[name="status"]')
// null
null. On a page that obviously had a dropdown. I could see it. I could click it. I could expand it with the mouse. But for some reason document.querySelector insisted it didn't exist.
This is a story about three layers of DOM that don't talk to each other, and what safari_select_option had to learn in v2.11.3 of Safari MCP to reach across them.
The setup
The page was a Salesforce/Lightning support form embedded in a customer portal. The portal is the parent document. The form is in an <iframe> that Lightning ships from a different (but same-origin) host. Inside that iframe, Lightning composes its UI from a graph of custom elements — each one with its own shadow root, each one with its own internal layout.
So when a developer writes a "Status" dropdown in Lightning, the actual <select> element ends up rendered inside something like:
top document
└── <iframe src="...lightning host...">
└── <support-form>
└── #shadow-root
└── <lightning-status-field>
└── #shadow-root
└── <select> ← the element my agent needed
document.querySelector('select[name="status"]'), called from the top document, traverses none of that. Not the iframe, not the shadow roots. To it, the <select> simply doesn't exist.
What was confusing
Two things made this hard to spot:
-
safari_snapshotsaw it. When the agent took an accessibility snapshot of the page, the dropdown was right there — with a ref, a label, anaria-expandedstate, options. Nothing felt missing. -
safari_clickworked. I'd been clicking deep-DOM elements for weeks without thinking about it. The button that opened this same form was inside a different shadow root, and click resolved it just fine.
So the agent kept asking itself: "I just clicked into this form. The dropdown is right there. Why can't I select anything?"
The answer, embarrassingly, was that click and select_option were using different finders.
Two finders, one tool that hadn't been told
Safari MCP ships two element-resolution paths inside the page:
-
mcpFindRef(ref)— given a ref fromsafari_snapshot, walk the document, every same-origin iframe, and every reachable shadow root to find the element that ref points to. -
mcpQuerySelectorDeep(selector)— given a CSS selector, do the same deep walk, but match by selector instead of ref.
Click had been using both of these for a long time. That's why click "just worked" on Lightning forms and React component libraries and modal dialogs that render into portals.
safari_select_option, meanwhile, was still doing this:
var el = document.querySelector(sel);
One line. Top frame only. No iframes, no shadow roots, no nothing. On any normal <select> on a normal page, that line is fine — and it had been fine since the day the tool was written, which is exactly why nobody had touched it.
But once a single user dropped Safari MCP into a real Salesforce portal, that line was wrong on every call.
The fix
The v2.11.3 patch is small. It teaches safari_select_option what safari_click already knew:
let finder;
if (ref) {
finder = `mcpFindRef('${ref}')`;
} else if (selector) {
finder = `(document.querySelector('${sel}')||mcpQuerySelectorDeep('${sel}'))`;
}
Two paths:
-
refpath — the one to use for anything found viasafari_snapshot. It resolves through the same deep walker as click. Iframes, shadow roots, both kinds of nested-component land. -
selectorpath — start with the cheap top-frame query (still right 95% of the time), fall through to the deep walker only when that returns null.
The rest of the tool — the _valueTracker reset that wakes up React's controlled-input bookkeeping, the value-then-text-then-substring matching for selects whose visible text doesn't equal their value, the input/change/blur event sequence — is unchanged. That part wasn't broken. The element lookup was.
Full v2.11.3 release notes here.
What I should have done earlier
The honest version of this is: I had two finders. I used one of them in click. I forgot it existed when I wrote select_option. The fix took an hour. The bug had been there for three months.
The lesson I keep relearning while building this tool is that DOM tools should not have a "happy path." Every tool that resolves an element should resolve it the same way as every other tool, because the page doesn't know which tool you're going to call next. Click into a shadow root, then try to fill a field, and the fill tool had better look in the same place click did.
safari_fill, safari_get_element, safari_hover, safari_get_computed_style — they all went through this same migration earlier in the v2.x series, one bug report at a time. safari_select_option was the last one I hadn't audited. v2.11.3 closes that gap.
If you're building any kind of browser-automation tool — MCP or otherwise — the question worth asking your codebase tonight is: do all my element-resolution paths agree on what "the element" is? Because the day they don't, the agent will be the one to find out.
Safari MCP is open source (MIT) at github.com/achiya-automation/safari-mcp. Native browser automation for AI agents on macOS — your real Safari, your real logins, no Chrome.
Top comments (0)