MCP Browser Automation: Why Rich-Text Editors Fail Silently
MCP

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?

What kind of business do you run?
What should the bot do?

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

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

⌨️ AI fakes typing into the editor
🚪 Boundary 1: focusoutClick-away to save closes the pop-up — text vanishes
🛡️ Boundary 2: isTrusted:falseEditor reads the truth badge · rejects the fake event · reports success anyway
🔁 Boundary 3: real OS pasteBringing the window forward fires focusout again → back to Boundary 1
❌ Log says "Filled" · the app saved nothing

“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 section2 min
❌ Faking keystrokes Dispatches synthetic events — a fake paste, a fake key press · the editor reads isTrusted:false and drops them · but dispatchEvent still returns cleanly, so the tool reports success · nothing is saved
✅ Editor-native commands Grabs the editor's own handle on the page and hands it the new text through the same commands its plugins, autosave, and undo already use · nothing is faked · focus never leaves the box · the change sticks

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 section1 min

Scroll sideways to see all columns.

Editor frameworkUsed bySynthetic events accepted?Editor API exposed?Recommended fill path
LexicalLinkedIn, Meta, Shopify❌ Silent reject__lexicalEditor on DOMeditor.setEditorState()
ProseMirrorNotion, Atlassian, Tiptap❌ Silent rejectpmViewDesc.viewview.dispatch(transaction)
Tiptap (ProseMirror wrapper)Many SaaS dashboards❌ Silent rejectel.editoreditor.commands.setContent()
Draft.jsJIRA, older Reddit, Facebook❌ Silent reject⚠️ React Fiber walkingFiber-walk to editorState
Slate.jsSome CMS dashboards❌ Silent reject✅ Editor instanceTransforms.insertText()
MonacoVS Code Web, GitHub editor✅ Mostly acceptsmonaco globalmodel.setValue()
CodeMirrorGitHub, GitLab code views✅ Mostly accepts✅ View instanceview.dispatch()
<textarea> / <input>Plain forms✅ AcceptsN/A (no editor)Standard fill() works
QuillSome older blogs⚠️ Partial✅ Quill instancequill.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 section1 min

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 section1 min

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:

  1. Can it post to LinkedIn when the AI is told to?
  2. Can it write a multi-line comment on a GitHub pull request and actually submit it?
  3. Can it fill in a Notion page with text that has formatting, not just plain words?
  4. 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 section4 min

Editor frameworks and their public deployment footprint

Scroll sideways to see all columns.

FrameworkPublic deployment (per framework or vendor docs)GitHub stars (May 2026)Rejects synthetic events?Documented entrypoint
LexicalLinkedIn (1B+ MAU per Meta investor reports, Shopify, Meta WhatsApp Web22K+ on lexical/lexicalYes, by designeditor.parseEditorState()
ProseMirrorNotion, Atlassian (JIRA, Confluence), New York Times, The Guardian7K+ on ProseMirror/prosemirrorYes (source: prosemirror-view/src/input.ts)view.dispatch(transaction)
Tiptap (ProseMirror wrapper)Many SaaS dashboards including Tally, Plane, Outline27K+ on ueberdosis/tiptapYes (inherits from ProseMirror)editor.commands.setContent()
Draft.jsReddit (legacy editor), older JIRA, formerly Facebook22K+ on facebook/draft-js (deprecated by Meta)YesReact Fiber → editorState
Slate.jsVarious CMS dashboards, design tools29K+ on ianstormtaylor/slateYesTransforms.insertText()
QuillSome older blog platforms44K+ on quilljs/quillPartial (older versions)quill.setText()
MonacoVS Code (browser + desktop), GitHub web editor40K+ on microsoft/monaco-editorMostly accepts (code editor, different threat model)model.setValue()
CodeMirror v6GitHub web (some surfaces), Replit, Observable27K+ on codemirror/devMostly 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 __lexicalEditor handle 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-state guide 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:

  1. 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).
  2. 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.
  3. 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.
  4. 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 section1 min

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 section2 min

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.

SymptomWrong first moveRight first move
React button opens dialogs but never commitsBuild native CGEvent clickingTry a Fiber-path click first
Handler never fires, no console errorAssume anti-bot detectionCheck whether the handler is on the Fiber node, not the DOM node
Works manually, fails automatedEscalate to OS-level inputConfirm 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 section1 min
Losing leads because no one's answering?

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 Quote

Prefer to chat? WhatsApp me · full pricing · our projects

Achiya - Business automation and bot specialist

Achiya Cohen

Business Automation Expert · Building bots since 2023

Completed 50+ automation projects — WhatsApp bots, CRM integrations, and automated workflows. Specializing in n8n, Make, and WhatsApp Business API.

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 Quote

Prefer WhatsApp? Message me →

I’ll reply as soon as I can · Project quote based on scope

Share this article:

Frequently Asked Questions

Why does my AI agent fail to post to LinkedIn, Notion, or Google Docs?
Almost every automation tool types into a page by faking the events a real keyboard or mouse would send. But the fancy text boxes on sites like LinkedIn, Notion, and Google Docs (built with tools called Lexical, ProseMirror, and Draft.js) are built to ignore fake typing. They check a little flag the browser attaches to every event that says whether a real person did it, and fake events fail that check — on purpose, so only a genuine human can change what you're about to publish. There are two ways around it: talk to the text box directly through its own built-in commands, or have the computer itself press Cmd+V for real, the same as if you tapped the keys yourself.
What is event.isTrusted and why does it matter?
Every time something happens on a web page — a key press, a click, a paste — the browser attaches a small true/false label to it called isTrusted. It says true when a real person did the action, and false when a script pretended to do it. There is no way for a script to fake a true. The fancy text boxes on sites like LinkedIn and Notion read that label and quietly throw away anything marked false, so that a hidden script can't change your text behind your back.
How does Safari MCP solve the rich-text editor problem?
Instead of faking keystrokes, Safari MCP (version 2.9.4 and up) simply asks the text box to change its own text using the text box's own built-in commands. For LinkedIn-style boxes (the Lexical kind) it calls the commands editor.parseEditorState() and setEditorState(); for Notion-style boxes (ProseMirror) it uses that editor's own change system, pmViewDesc.view.dispatch(). Because nothing is faked, the text box updates its content directly and treats the change as legitimate.
Is this a security vulnerability in browser automation?
No — this is the text box doing exactly what it's supposed to do. Rejecting fake events is a deliberate safety feature that stops a malicious script from changing your text without you noticing. When automation is done the right way, it isn't sneaking around that feature; it's using the same built-in commands the page itself already uses to update its own text. The line is simple: pretending to be a person gets blocked, while asking the text box to change itself is allowed.
Does this affect Playwright, Puppeteer, and Chrome DevTools MCP?
Yes, all of them, though some handle it a little better than others. By default, all three fill in forms by faking keystrokes. Playwright has added special handling for a couple of code-editing boxes (Monaco and CodeMirror), but the LinkedIn-style and Notion-in-a-popup boxes still fail quietly in most automation tools. This isn't a flaw in any one tool — it's simply how modern text boxes work: they check whether the typing came from a real person, and fake typing doesn't pass that check no matter which tool sent it.
Let's talk?