DEV Community

Cover image for My agent could see the dropdown. It just couldn't pick anything.
אחיה כהן
אחיה כהן

Posted on

My agent could see the dropdown. It just couldn't pick anything.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. safari_snapshot saw it. When the agent took an accessibility snapshot of the page, the dropdown was right there — with a ref, a label, an aria-expanded state, options. Nothing felt missing.
  2. safari_click worked. 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 from safari_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);
Enter fullscreen mode Exit fullscreen mode

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}'))`;
}
Enter fullscreen mode Exit fullscreen mode

Two paths:

  • ref path — the one to use for anything found via safari_snapshot. It resolves through the same deep walker as click. Iframes, shadow roots, both kinds of nested-component land.
  • selector path — 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)