MCP Browser Automation: Why Rich-Text Editors Fail Silently
When an AI tries to type into the fancy text boxes on LinkedIn, Notion, Google Docs, or JIRA, it often fails without anyone noticing — the log says it worked and the screenshot even shows the words, but nothing is actually saved. The automation tools this happens to include Playwright, Puppeteer, Chrome DevTools, and Browserbase, all of which are part of MCP (Model Context Protocol), the standard AI assistants use to control a browser. Three separate safety features cause it: the text box closes the moment focus leaves it (focusout dismissing dialogs), the box ignores fake typing (isTrusted:false rejection in editor paste handlers), and even a genuine copy-paste can knock focus away and close the box (OS-level paste re-triggering focusout). To make it work you either use each text box’s own built-in commands, or press Cmd+V for real at the operating-system level — not a faked event (dispatchEvent).
When you tell one of these tools to fill in or type text, it doesn’t really press keys — it manufactures the little signals a browser fires when you type, known as synthetic DOM events (in plain terms, fake typing). For an ordinary text field or a simple form, that’s perfectly fine and it just works. But for the richer text boxes that sit inside pop-up windows — LinkedIn’s post composer, Notion’s page editor, JIRA’s comment box, Google Docs — it fails quietly, in a way that’s almost impossible to spot from the outside.
The failure rate depends on the editor, the browser-control method, and the application’s save logic. There is no published benchmark in this article that supports a universal split. Treat every rich editor as a separate integration and verify the value after writing it.
So here’s what you see. The AI says it worked. The log says “Filled”. The words even flash on the screen for a second. Then the pop-up closes, the text is gone, and you’re left wondering why your AI can’t seem to post anything.
The rest of this article walks through exactly how that failure happens — and why fixing it means understanding three separate safety features that most automation tools simply don’t plan for.
“MCP is an open protocol that standardizes how applications provide context to LLMs. Think of MCP like a USB-C port for AI applications.” — Anthropic, Introducing the Model Context Protocol
Want a quick answer for your business?
Choose your business and what you need to see a recommendation — no form or contact details required.
Table reservations, a digital menu, takeaway orders and arrival reminders — without leaving the kitchen to answer the phone. Explore the solution for your business
Appointment scheduling, automatic reminders and a waiting list that fills cancelled slots. Explore the solution for your business
Appointment scheduling and reminders. Among our clients, reminders reduced cancellations by about 50%. Explore the solution for your business
Automatic lead qualification, property details and tour scheduling — before you have time to reply. Explore the solution for your business
Handle enquiries 24/7, schedule meetings and send documents, with a clear record of every enquiry. Explore the solution for your business
Abandoned cart recovery and order updates. About 70% of online shopping carts are abandoned. Explore the solution for your business
Basic botAround ₪3,500 one-time
Answers recurring questions, sends prices and information, and hands over to a person when needed.
Business packageAround ₪6,500 one-time
Connects to your calendar and CRM, books appointments and can accept payments.
Full AI solutionUp to ₪12,000 one-time
Understands natural language and holds a conversation, connected to several systems at once.
Monthly costs are the same across package levels and depend on the technical route: ₪100–300 for the economical route (typically ₪150), or ₪315–890 for the official Meta API route. Full details in the pricing guide
The three boundaries
focusoutClick-away to save closes the pop-up — text vanishesisTrusted:falseEditor reads the truth badge · rejects the fake event · reports success anywayfocusout again → back to Boundary 1“Lexical is an extensible JavaScript web text-editor framework. It’s designed for reliability, accessibility, and performance.” — Meta, Lexical project documentation
Boundary 1: focusout dismisses the dialog
Text boxes that live inside pop-up windows — LinkedIn’s post box, Shopify’s product-description editor, Notion’s inline editor — watch for the moment your cursor leaves the box. In browser terms that moment is the focusout event. They watch for it for a good reason: if you click somewhere else on the page, they assume you’re done and politely close the pop-up.
Here’s the trap. Many automation tools finish typing by deliberately clicking away from the box — a step called element.blur() — so the page will notice the text and save it. But clicking away is exactly what fires focusout. The pop-up thinks you left, so it closes, and the text you just entered vanishes right along with it.
Plenty of teams have burned hours on the mystery of “my text shows up for a second and then disappears.” The fix is simply to not click away: React (the code running the page) already notices the new text from the typing events by themselves, so the extra click-away step isn’t needed.
Boundary 2: isTrusted:false rejection in editor paste handlers
“The isTrusted read-only property of the Event interface is a Boolean value that is true when the event was generated by the user agent (including via user actions and programmatic methods such as HTMLElement.focus()), and false when the event was dispatched via EventTarget.dispatchEvent().” — MDN Web Docs, Event.isTrusted
In plain terms, event.isTrusted is a truth badge the browser pins to every event. It reads true only when the browser itself set the event off — a real key press, a real paste, a real click. Anything a script cooks up gets stamped isTrusted:false, and there’s no way for the script to forge a true.
The fancy text boxes read that badge on purpose and turn away anything that isn’t genuine. Notion’s paste handler, LinkedIn’s clipboard code, JIRA’s typing code (built on ProseMirror, Lexical, and Draft.js) all refuse fake events on purpose. It isn’t a bug — it’s what stops a sneaky script from swapping out the text in a box you’re about to sign or publish.
None of the usual fake-typing tricks carry a genuine badge — not a fake paste (ClipboardEvent('paste')), not a fake keystroke (InputEvent('beforeinput')), not the old-school execCommand('insertText') command. So the tool’s attempt just bounces off the text box, yet it still reports success. Why? Because the act of firing the event (dispatchEvent) finishes cleanly on its own — the tool never hears that the text box quietly ignored it.
Boundary 3: Native OS paste triggers Boundary 1
The usual way around Boundary 2 is to stop faking anything and have the computer itself do a real paste — for example, put the text on the clipboard and then have the operating system genuinely press Cmd+V (via AppleScript on a Mac, or the Windows equivalent), exactly as if you’d tapped the keys.
And it does work — the browser sees a real paste, the truth badge (isTrusted) reads true, and the text box happily accepts the content. But there’s a catch. To send that keystroke to the right window, the tool first has to pull the target browser to the front of the screen. The instant it does, whatever app was in front loses focus — and losing focus is the very focusout event from Boundary 1. So we’ve come full circle.
The paste does go through, just into the wrong place: by the time it lands, the pop-up has already snapped shut.
Why this matters for AI agents
When an AI is set loose to create content in a browser — post to LinkedIn, reply on a GitHub pull request, write up a Notion page, open a JIRA ticket — it runs into all three of these safety features over and over. And the frustrating part is that failure looks exactly like success: the tool says “filled”, the text flashes on screen, and the AI moves on to the next step. The only place the truth shows up is in what the app actually saved — which is the one thing the user cares about, and it’s empty. This is the same trap we cover in our broader AI agents for business guide: a system that says “done” without checking whether it really happened can’t be trusted in the real world.
This is especially damaging when a chain of steps runs on its own, because nothing ever raises a hand to say something went wrong. Every later step just carries on as if the post went up. The AI reports that it finished. And the person only finds out hours later — usually when a customer asks about the LinkedIn announcement that was never actually published.
The real fix: editor-native API access
Every one of the three safety features above kicks in because the tool is pretending to be a person. So the real answer is the obvious one: stop pretending. Instead of faking keystrokes, reach the text box directly and ask it to change its own text using the controls it already has built in.
Read the section
isTrusted:false and drops them · but dispatchEvent still returns cleanly, so the tool reports success · nothing is saved
That’s easier than it sounds, because these text boxes leave a handle to themselves right there on the page. For the Lexical kind (the boxes behind LinkedIn, Meta, and Shopify), the code below grabs that handle and hands the box its new text directly — no faking involved:
👨💻 Show the code (for developers)
const editorEl = document.querySelector('[data-lexical-editor="true"]');
const editor = editorEl.__lexicalEditor;
const newState = editor.parseEditorState(lexicalJson);
editor.setEditorState(newState);
ProseMirror (the box behind Notion and Atlassian) works the same way, just through a different handle called pmViewDesc; this snippet reaches it and inserts the text through the editor’s own change system:
👨💻 Show the code (for developers)
const view = editorEl.pmViewDesc.view;
const tr = view.state.tr.insertText(text, view.state.selection.from);
view.dispatch(tr);
Draft.js — an older one that’s officially retired but still lurking in GitHub, Reddit, and some older Facebook screens — hides its handle a little deeper, inside React’s own internal map of the page (a technique called “Fiber walking”), but the idea is identical.
Notice what none of these do: none of them fake an event. The text box changes its own text directly, the page redraws the way it normally would, the box’s own rules stay intact, and — crucially — the pop-up stays open because your cursor never left it, so there’s no focusout to trigger Boundary 1.
And this isn’t some fragile trick. These are the exact same controls the text box’s own features rely on — its plugins, its autosave, its undo and redo. They aren’t written up in the official manual, but they’re rock-solid: the sites themselves have leaned on these same controls in production for years.
Editor framework comparison — what works, what fails
Different websites build their text boxes with different underlying tools, and each tool behaves differently when an AI tries to type into it. The table below lines them up side by side: which big sites use each one, whether it accepts fake typing, whether it leaves a handle you can grab, and the cleanest way to fill it. If you only remember one column, make it the third — “Synthetic events accepted?” tells you at a glance whether fake typing will silently fail.
Read the section
Scroll sideways to see all columns.
| Editor framework | Used by | Synthetic events accepted? | Editor API exposed? | Recommended fill path |
|---|---|---|---|---|
| Lexical | LinkedIn, Meta, Shopify | ❌ Silent reject | ✅ __lexicalEditor on DOM | editor.setEditorState() |
| ProseMirror | Notion, Atlassian, Tiptap | ❌ Silent reject | ✅ pmViewDesc.view | view.dispatch(transaction) |
| Tiptap (ProseMirror wrapper) | Many SaaS dashboards | ❌ Silent reject | ✅ el.editor | editor.commands.setContent() |
| Draft.js | JIRA, older Reddit, Facebook | ❌ Silent reject | ⚠️ React Fiber walking | Fiber-walk to editorState |
| Slate.js | Some CMS dashboards | ❌ Silent reject | ✅ Editor instance | Transforms.insertText() |
| Monaco | VS Code Web, GitHub editor | ✅ Mostly accepts | ✅ monaco global | model.setValue() |
| CodeMirror | GitHub, GitLab code views | ✅ Mostly accepts | ✅ View instance | view.dispatch() |
<textarea> / <input> | Plain forms | ✅ Accepts | N/A (no editor) | Standard fill() works |
| Quill | Some older blogs | ⚠️ Partial | ✅ Quill instance | quill.setText() |
The table is a compatibility checklist, not a frequency study. Lexical, ProseMirror, Tiptap, Draft.js, and Slate.js need editor-specific testing, while code editors and plain inputs follow different event paths. In every case, read the saved value back before reporting success.
How to benchmark an editor honestly
This article does not publish a reproducible dataset for cross-editor failure rates, latency, or cost. Without the raw attempts, environment, version locks, and read-back results, those numbers would look precise without being auditable.
Read the section
For a real benchmark, record the editor and browser versions, the exact fill method, the value written, the value read back after save, the failure mode, and the time spent retrying. Run the same protocol on every surface and publish the raw observations. Until then, describe compatibility as tested or untested rather than attaching a success percentage.
The operational rule is simpler: after writing, read the saved content back from the application. If the target uses Lexical, ProseMirror, Tiptap, Draft.js, or Slate.js, test the editor’s own command path instead of assuming synthetic typing worked.
If you want the wider engineering picture — why these failures pile up across a chain of automated steps and how to keep an eye on them — see our n8n vs Make vs Zapier comparison, which looks at how well each tool lets you see what’s actually happening.
What to verify when evaluating MCP browser tools
According to Aggarwal et al. in the GEO study (arXiv:2311.09735), adding direct quotations increased citation likelihood by 43% in their experiment, the largest measured change among the six strategies they tested.
Read the section
If you’re shopping for a browser-automation tool (an MCP server) to let your AI create content, don’t take the marketing at face value — put it through these four tests, which are exactly the cases that tend to break:
- Can it post to LinkedIn when the AI is told to?
- Can it write a multi-line comment on a GitHub pull request and actually submit it?
- Can it fill in a Notion page with text that has formatting, not just plain words?
- Can it open a JIRA ticket with a real, formatted description rather than bare text?
If any of these quietly fails — it reports “success” but the app shows nothing — you’ve caught one of the three safety features blocking the tool.
Where this fails in practice: documented framework behavior
None of this is guesswork — you can read it right in each text box tool’s own source code, where it openly refuses fake typing. The table below pairs up where each tool is used in the wild (the well-known apps that have said so publicly) with the refusal behavior spelled out in that tool’s own code repository.
Read the section
Editor frameworks and their public deployment footprint
Scroll sideways to see all columns.
| Framework | Public deployment (per framework or vendor docs) | GitHub stars (May 2026) | Rejects synthetic events? | Documented entrypoint |
|---|---|---|---|---|
| Lexical | LinkedIn (1B+ MAU per Meta investor reports, Shopify, Meta WhatsApp Web | 22K+ on lexical/lexical | Yes, by design | editor.parseEditorState() |
| ProseMirror | Notion, Atlassian (JIRA, Confluence), New York Times, The Guardian | 7K+ on ProseMirror/prosemirror | Yes (source: prosemirror-view/src/input.ts) | view.dispatch(transaction) |
| Tiptap (ProseMirror wrapper) | Many SaaS dashboards including Tally, Plane, Outline | 27K+ on ueberdosis/tiptap | Yes (inherits from ProseMirror) | editor.commands.setContent() |
| Draft.js | Reddit (legacy editor), older JIRA, formerly Facebook | 22K+ on facebook/draft-js (deprecated by Meta) | Yes | React Fiber → editorState |
| Slate.js | Various CMS dashboards, design tools | 29K+ on ianstormtaylor/slate | Yes | Transforms.insertText() |
| Quill | Some older blog platforms | 44K+ on quilljs/quill | Partial (older versions) | quill.setText() |
| Monaco | VS Code (browser + desktop), GitHub web editor | 40K+ on microsoft/monaco-editor | Mostly accepts (code editor, different threat model) | model.setValue() |
| CodeMirror v6 | GitHub web (some surfaces), Replit, Observable | 27K+ on codemirror/dev | Mostly accepts (code editor) | view.dispatch() |
What the pattern tells us: of the 8 tools above, 6 turn away fake typing on purpose (Lexical, ProseMirror, Tiptap, Draft.js, Slate.js, and Quill in part), and 2 let it through (Monaco, CodeMirror). That split isn’t a coincidence. The 6 that refuse are all publishing boxes — the kind where you write things you’re about to share, so they assume a hostile script might try to slip in content. The 2 that accept are code boxes, built for developers, where the assumption is you’re using your own tools.
Why the rejection is intentional (not a bug)
Each of these tools explains the reasoning right in its own code:
- Lexical: that
__lexicalEditorhandle is there on purpose — so the box’s own features and the page’s normal update cycle can make changes the box trusts. Let fake outside events in, and they’d skip the checks the box runs and quietly break its undo/redo history. See Lexical editor state architecture. - ProseMirror: the only legitimate way to change the text is through the box’s own bundled change operations (it calls them transactions) — see
prosemirror-stateguide on transactions. Fake typing skips the safety checks that keep the content from getting mangled. - Draft.js (now retired): it keeps its text in a locked container that can only be swapped out through React’s own update machinery — there’s simply no back door for a fake event. It was officially retired in 2022 (Meta blog post), which leaves it running in old-mode on holdouts like JIRA that never moved off it.
What the empirical literature says about this class of failure
This isn’t a quirk of one browser, either — the isTrusted truth badge is written into the official web rulebooks: the W3C UIEvents spec and, again, the WHATWG DOM standard. Both say the same thing: you’re allowed to fire a fake event, but it carries a “not genuine” badge (isTrusted:false) that a text box is free to reject.
And it’s a well-worn headache for people building AIs. The Anthropic MCP design discussion and the browser-use issue tracker both have running threads about this exact problem — search any browser-automation project for “silent failure” plus “rich text” and you’ll see it come up again and again.
A practical detection checklist
Since none of these text boxes ever report an error (firing the event finishes cleanly even when the box ignored it), you can’t sit back and wait for a warning — you have to go check the result yourself. Here are four ways to do that, from simplest to most thorough:
- Go back and look at the page — reopen the post’s own URL and confirm the text is actually there. This catches Boundary 1 (the pop-up closed before saving) and Boundary 3 (the paste landed in the wrong place).
- Ask the platform directly — after posting, use the site’s API (the official way one program asks another for information) to check that the post really exists on LinkedIn, Notion, or JIRA. This is the one check that reliably catches all three safety features.
- Watch the box for a change, and give up after 30 seconds — keep an eye on the text box (technically, watch the page’s structure, the DOM, for updates); if nothing that looks like a save shows up within 30 seconds, count it as failed. This reliably catches Boundary 2.
- Have a second account look — for posts that really matter (scheduled announcements, messages to customers), get a different account to confirm the content is visible. It’s the most trustworthy check, but it takes extra time.
The thread running through all of them: never trust the “success” message on its own. The very fact that firing the event finishes cleanly is exactly why this kind of failure slips past most logging and monitoring.
Safari MCP and the Lexical path
Safari MCP does exactly that for the Lexical boxes as of version 2.9.4 — it fills them the honest way, through the box’s own commands. As far as I know it’s the only such tool that can handle LinkedIn’s post box from start to finish with no help from you, and the same release adds the Notion-style (ProseMirror) path too.
Read the section
Safari MCP is free and open source (under the MIT license), you start it by running npx safari-mcp, and it works with Claude Code, Cursor, VS Code, Windsurf, Claude Desktop, and any other MCP-compatible AI on a Mac.
Every serious browser-automation tool is going to have to crack this sooner or later — these fancy text boxes are simply too common to keep leaving them as a place where things fail in silence. Until then, if you’re staring at “the AI says it worked but nothing got posted,” the three safety features above are the first place to look.
To see where these automations actually earn their keep in a business, read our WhatsApp Business automation guide and the wider business automation framework, which cover how to build these result-checks right into your workflows. And for more on designing AI-driven systems, see our deep-dives on AI agents for business, AI in business overview, and Safari MCP browser automation.
Update (August 2026): the React Fiber path around isTrusted
Added August 26, 2026, from a live debugging session on Meta’s Business Settings UI.
Read the section
Boundary 2 above describes editors that reject synthetic events by testing event.isTrusted.
The natural conclusion — the one I reached myself, and acted on for far too long — is that such
a control needs a real OS-level click, and that the fix therefore lives in CGEvent,
accessibility AXPress, or some other native-input layer.
That conclusion is wrong for React controls, and it is an expensive wrong turn.
React does not attach your onClick to the DOM node. It keeps handlers in its own internal
tree — the Fiber tree — and dispatches them through a synthetic event system of its own. An
automation tool that walks to the element’s Fiber node and invokes the stored onClick
directly is making a function call, not a DOM event. There is no event object for a guard
to inspect, so an isTrusted check never runs at all. The guard is not defeated; it is simply
never reached.
Worked example (verified August 25, 2026). Adding a use case to an app in Meta’s Business Settings: every raw synthetic click I dispatched opened the dialog and ticked the checkboxes, but the Save button silently did nothing — the classic Boundary 2 signature. No native click was needed. A Fiber-path click saved the record, confirmed by reloading the page and seeing the use case listed as active.
What this changes about the diagnosis
Scroll sideways to see all columns.
| Symptom | Wrong first move | Right first move |
|---|---|---|
| React button opens dialogs but never commits | Build native CGEvent clicking | Try a Fiber-path click first |
| Handler never fires, no console error | Assume anti-bot detection | Check whether the handler is on the Fiber node, not the DOM node |
| Works manually, fails automated | Escalate to OS-level input | Confirm which event system actually owns the handler |
The ordering that has served me best: Fiber-path click → precise element reference → only then
consider native input. On macOS 26 that last step is largely theoretical anyway, since
CGEvent synthetic clicks are a no-op under the current input-security model — another reason
not to start there.
The broader point for anyone evaluating MCP browser tools: “does it send a trusted event?”
is the wrong question. The right one is “does it reach the handler the framework actually
registered?” A tool that understands the framework’s event system beats one that tries to look
more human to the DOM. The isTrusted wall in Boundary 2 is real, but for React it is usually
a wall with a door next to it.
Sources & References
Each of the three safety features above comes straight from the security design that these tools spell out in their own documentation:
Read the section
- Lexical editor state — official architecture doc — Meta’s own explanation of how the box stores its text
- Lexical commands API — the official, approved way to talk to the box
- ProseMirror state & transactions — why simply setting
.value =never reaches this kind of box - Tiptap (built on ProseMirror) —
editor.commands.setContent— the recommended command for changing its text - Draft.js
EditorState— Facebook’s now-retired text-box tool (still living on inside JIRA) InputEvent.isTrustedspec (W3C DOM) — the truth badge that lets a box reject fake typing- Model Context Protocol (MCP) spec — Anthropic’s open standard for letting AI use tools
- Chrome DevTools Protocol —
Input.dispatchKeyEvent— Chrome’s closest equivalent for sending keystrokes - Safari MCP project — the LinkedIn (Lexical) and Notion (ProseMirror) fill paths described above
A WhatsApp bot can run defined replies, appointment steps and lead capture outside office hours, subject to connected-system availability. Projects start from $1,000 one-time. Tell me about your business →
Get a Custom QuotePrefer to chat? WhatsApp me · full pricing · our projects
Ready to automate your business?
50+ automation projects completed. Tell me about yours — I'll show you exactly what we can automate.
Get a Custom QuoteI’ll reply as soon as I can · Project quote based on scope