惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
腾讯CDC
爱范儿
爱范儿
Google DeepMind News
Google DeepMind News
V
V2EX
Blog — PlanetScale
Blog — PlanetScale
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI
量子位
F
Fortinet All Blogs
G
Google Developers Blog
T
The Blog of Author Tim Ferriss
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Hugging Face - Blog
Hugging Face - Blog
Last Week in AI
Last Week in AI
T
Tailwind CSS Blog
J
Java Code Geeks
S
SegmentFault 最新的问题
D
Docker
博客园 - 司徒正美
The GitHub Blog
The GitHub Blog
Jina AI
Jina AI
M
MIT News - Artificial intelligence
博客园 - 【当耐特】

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
My agent could see the dropdown. It just couldn't pick an...
אחיה כהן · 2026-05-24 · via DEV Community

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.