BareGit

Fix JavaScript evaluation under page CSP

Use Firefox's mapped-await protocol support for promises and retry scripts as
statement bodies without relying on CSP-blocked dynamic function compilation.
Author: MetroWind <chris.corsair@gmail.com>
Date: Fri Sep 4 09:41:37 2026 -0700
Commit: 1f4820afbc42f28134081260bcf6c8176151b3a7

Changes

diff --git a/mcp/firefox-rdp/src/browser_session.py b/mcp/firefox-rdp/src/browser_session.py
index 24d5b79..12ddc5f 100644
--- a/mcp/firefox-rdp/src/browser_session.py
+++ b/mcp/firefox-rdp/src/browser_session.py
@@ -365,10 +365,19 @@ class BrowserSession:
         async with self._operation(cancelled):
             await self._prepareSelected()
             self._checkCancelled(cancelled)
-            value = await self._evaluateEnvelope(
-                page_scripts.runJavaScript(script),
-                timeout_ms=timeout_ms,
-            )
+            try:
+                value = await self._evaluateEnvelope(
+                    page_scripts.runJavaScript(script),
+                    timeout_ms=timeout_ms,
+                )
+            except PageScriptError:
+                value = await self._evaluateEnvelope(
+                    page_scripts.runJavaScript(
+                        script,
+                        statement_body=True,
+                    ),
+                    timeout_ms=timeout_ms,
+                )
             self._checkCancelled(cancelled)
             await self._refresh()
             return value
diff --git a/mcp/firefox-rdp/src/page_scripts.py b/mcp/firefox-rdp/src/page_scripts.py
index b1af543..a87b8c5 100644
--- a/mcp/firefox-rdp/src/page_scripts.py
+++ b/mcp/firefox-rdp/src/page_scripts.py
@@ -20,28 +20,21 @@ def _wrapped(body: str) -> str:
 
 def _wrappedAsync(body: str) -> str:
     return (
-        "await (async () => { try { " + body
+        "(async () => { 'use strict'; try { " + body
         + " } catch (error) { return JSON.stringify({ok:false,error:{"
         + "code:'javascript_error',message:String(error && "
         + "error.message || error)}}); } })()"
     )
 
 
-def runJavaScript(script: str) -> str:
+def runJavaScript(script: str, *, statement_body: bool = False) -> str:
     """Build an async evaluator for caller-supplied page JavaScript."""
+    if statement_body:
+        operation = f"(async () => {{\n{script}\n}})()"
+    else:
+        operation = f"(\n{script}\n)"
     body = f"""
-const source = {_literal(script)};
-const AsyncFunction = Object.getPrototypeOf(async function() {{}})
-    .constructor;
-let operation;
-try {{
-    operation = new AsyncFunction(
-        '\"use strict\"; return (\\n' + source + '\\n);'
-    );
-}} catch (expressionError) {{
-    operation = new AsyncFunction('\"use strict\";\\n' + source);
-}}
-const result = await operation();
+const result = await {operation};
 const resultType = result === null ? 'null' : typeof result;
 let serializable = result;
 if (resultType === 'undefined') serializable = null;
diff --git a/mcp/firefox-rdp/src/rdp_connection.py b/mcp/firefox-rdp/src/rdp_connection.py
index 77a7e73..848f9b4 100644
--- a/mcp/firefox-rdp/src/rdp_connection.py
+++ b/mcp/firefox-rdp/src/rdp_connection.py
@@ -343,7 +343,7 @@ class RdpConnection:
                 "to": console_actor_id,
                 "type": "evaluateJSAsync",
                 "text": expression,
-                "options": {"eager": True},
+                "mapped": {"await": True},
             }
             try:
                 await self._websocket.sendText(
diff --git a/mcp/firefox-rdp/tests/manual_firefox_smoke.py b/mcp/firefox-rdp/tests/manual_firefox_smoke.py
index 09eb7c6..c61798a 100644
--- a/mcp/firefox-rdp/tests/manual_firefox_smoke.py
+++ b/mcp/firefox-rdp/tests/manual_firefox_smoke.py
@@ -157,7 +157,7 @@ async def runSmoke(firefox_binary: str) -> dict[str, object]:
         browser = BrowserSession(config, rdp)
         await browser.selectTab(tab_id=actor_id)
         javascript = await browser.runJavaScript(
-            "document.title",
+            "return document.title;",
             config.request_timeout_ms,
         )
         if javascript != {
diff --git a/mcp/firefox-rdp/tests/test_browser_session.py b/mcp/firefox-rdp/tests/test_browser_session.py
index 55d6a0b..0251a91 100644
--- a/mcp/firefox-rdp/tests/test_browser_session.py
+++ b/mcp/firefox-rdp/tests/test_browser_session.py
@@ -15,6 +15,7 @@ from src.config import Config
 from src.errors import (
     CancelledError,
     JavaScriptError,
+    PageScriptError,
     StaleElementError,
     UploadFileInvalidError,
     UploadTooLargeError,
@@ -52,7 +53,10 @@ class FakeRdp:
     async def evaluateJson(self, actor, expression, timeout_ms=None):
         """Pop the next scripted page result."""
         self.evaluations.append((actor, expression, timeout_ms))
-        return self.envelopes.pop(0)
+        result = self.envelopes.pop(0)
+        if isinstance(result, Exception):
+            raise result
+        return result
 
     async def listTabs(self):
         """Return one stable fixture tab for selected-page checks."""
@@ -179,6 +183,22 @@ class JavaScriptTest(unittest.IsolatedAsyncioTestCase):
         ):
             await browser.runJavaScript("throw new Error()", 1000)
 
+    async def test_retries_a_statement_body_after_parse_failure(self) -> None:
+        envelope = {
+            "ok": True,
+            "value": {"result": "Fixture", "result_type": "string"},
+        }
+        rdp = FakeRdp([PageScriptError("syntax error"), envelope])
+        browser = self.browser(rdp)
+        result = await browser.runJavaScript(
+            "return document.title;",
+            1000,
+        )
+        self.assertEqual(result, envelope["value"])
+        self.assertEqual(len(rdp.evaluations), 2)
+        self.assertNotIn("async () => {\nreturn", rdp.evaluations[0][1])
+        self.assertIn("async () => {\nreturn", rdp.evaluations[1][1])
+
 
 class FilesystemSecurityTest(unittest.TestCase):
     """Verify upload path, type, symlink, and size policy."""
diff --git a/mcp/firefox-rdp/tests/test_page_scripts.py b/mcp/firefox-rdp/tests/test_page_scripts.py
index a4725c1..ef1529b 100644
--- a/mcp/firefox-rdp/tests/test_page_scripts.py
+++ b/mcp/firefox-rdp/tests/test_page_scripts.py
@@ -67,20 +67,26 @@ class PageScriptsTest(unittest.TestCase):
         self.assertNotIn("requestSubmit", script)
         self.assertNotIn("form.submit", script)
 
-    def test_javascript_source_is_passed_as_a_json_literal(self) -> None:
-        source = "'); window.pwned = true; ('\nreturn await value;"
-        script = page_scripts.runJavaScript(source)
-        encoded = json.dumps(
-            source,
-            ensure_ascii=False,
-            separators=(",", ":"),
-        )
-        self.assertIn(f"const source = {encoded};", script)
-        self.assertIn("new AsyncFunction", script)
-        self.assertIn("const result = await operation();", script)
+    def test_javascript_source_does_not_require_dynamic_compilation(
+        self,
+    ) -> None:
+        script = page_scripts.runJavaScript("document.title")
+        self.assertIn("const result = await (\ndocument.title\n);", script)
+        self.assertNotIn("Function", script)
         self.assertIn("result_type:resultType", script)
         self.assertIn("code:'javascript_error'", script)
-        self.assertTrue(script.startswith("await (async () =>"))
+        self.assertTrue(script.startswith("(async () =>"))
+
+    def test_javascript_statement_body_runs_in_an_async_function(self) -> None:
+        script = page_scripts.runJavaScript(
+            "return document.title;",
+            statement_body=True,
+        )
+        self.assertIn(
+            "const result = await (async () => {\n"
+            "return document.title;\n})()",
+            script,
+        )
 
 
 if __name__ == "__main__":
diff --git a/mcp/firefox-rdp/tests/test_rdp_connection.py b/mcp/firefox-rdp/tests/test_rdp_connection.py
index 363c1a5..4e45bb0 100644
--- a/mcp/firefox-rdp/tests/test_rdp_connection.py
+++ b/mcp/firefox-rdp/tests/test_rdp_connection.py
@@ -79,6 +79,12 @@ class RdpConnectionTest(unittest.IsolatedAsyncioTestCase):
     async def test_routes_fast_evaluation_completion(self) -> None:
         result = await self.connection.evaluateJson("console", "script")
         self.assertEqual(result, {"ok": True, "value": {"x": 1}})
+        request = next(
+            packet for packet in self.websocket.sent
+            if packet["type"] == "evaluateJSAsync"
+        )
+        self.assertEqual(request["mapped"], {"await": True})
+        self.assertNotIn("options", request)
 
     async def test_does_not_treat_actor_notification_as_reply(self) -> None:
         loop = asyncio.get_running_loop()