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()