Skip to content

Commit b5c61de

Browse files
committed
add back the playwright mcp; update node version in coderunner
1 parent 8e619cb commit b5c61de

File tree

3 files changed

+65
-22
lines changed

3 files changed

+65
-22
lines changed

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ RUN python -m pip install --no-cache-dir --upgrade pip
5151
COPY ./requirements.txt /app/requirements.txt
5252

5353
# Install Python dependencies
54-
RUN pip install --no-cache-dir -r requirements.txt
54+
RUN pip install -r requirements.txt
5555

5656

5757
# Install the bash kernel spec for Jupyter (not working with uv)
@@ -89,7 +89,7 @@ COPY entrypoint.sh /entrypoint.sh
8989
RUN chmod +x /entrypoint.sh
9090

9191
# Ensure Node.js, npm (and npx) are set up
92-
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
92+
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
9393
RUN apt-get install -y nodejs
9494

9595
ENV PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ mcp[cli]
3434
fastmcp
3535

3636
openai-agents
37+
38+
playwright==1.53.0

server.py

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111

1212
import aiofiles
1313
import websockets
14-
from mcp.server.fastmcp import FastMCP
15-
14+
# Import Context for progress reporting
15+
from mcp.server.fastmcp import FastMCP, Context
16+
from playwright.async_api import async_playwright
17+
from bs4 import BeautifulSoup
18+
import socket
1619
# --- CONFIGURATION & SETUP ---
1720
logging.basicConfig(
1821
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
@@ -76,20 +79,21 @@ def create_jupyter_request(code: str) -> tuple[str, str]:
7679

7780

7881
# --- MCP TOOLS ---
79-
8082
@mcp.tool()
81-
async def execute_python_code(command: str) -> str:
83+
async def execute_python_code(command: str, ctx: Context) -> str:
8284
"""
83-
Executes a string of Python code in a persistent Jupyter kernel and returns the output.
84-
This is suitable for calculations, data analysis, and interacting with previously defined variables.
85+
Executes a string of Python code in a persistent Jupyter kernel and returns the final output.
86+
Streams intermediate output (stdout) as progress updates.
8587
8688
Args:
8789
command: The Python code to execute as a single string.
90+
ctx: The MCP Context object, used for reporting progress.
8891
"""
8992
# 1. Get Kernel ID
9093
if not os.path.exists(KERNEL_ID_FILE_PATH):
91-
logger.error(f"Kernel ID file not found at: {KERNEL_ID_FILE_PATH}")
92-
return "Error: Kernel is not running. The kernel ID file was not found."
94+
error_msg = f"Error: Kernel is not running. The kernel ID file was not found at: {KERNEL_ID_FILE_PATH}"
95+
logger.error(error_msg)
96+
return error_msg
9397

9498
with open(KERNEL_ID_FILE_PATH, 'r') as file:
9599
kernel_id = file.read().strip()
@@ -99,7 +103,7 @@ async def execute_python_code(command: str) -> str:
99103

100104
# 2. Connect and Execute via WebSocket
101105
jupyter_ws_url = f"{JUPYTER_WS_URL}/api/kernels/{kernel_id}/channels"
102-
output_lines = []
106+
final_output_lines = []
103107
sent_msg_id = None
104108

105109
try:
@@ -109,50 +113,87 @@ async def execute_python_code(command: str) -> str:
109113
logger.info(f"Sent execute_request (msg_id: {sent_msg_id})")
110114

111115
execution_complete = False
112-
loop_timeout = 3600.0 # Total time to wait for a result
116+
loop_timeout = 3600.0
113117
start_time = time.time()
114118

115119
while not execution_complete and (time.time() - start_time) < loop_timeout:
116120
try:
117-
# Wait for a message with a short timeout to keep the loop responsive
118121
message_str = await asyncio.wait_for(jupyter_ws.recv(), timeout=1.0)
119122
except asyncio.TimeoutError:
120123
continue
121124

122125
message_data = json.loads(message_str)
123126
parent_msg_id = message_data.get("parent_header", {}).get("msg_id")
124127

125-
# Ignore messages not related to our request
126128
if parent_msg_id != sent_msg_id:
127129
continue
128130

129131
msg_type = message_data.get("header", {}).get("msg_type")
130132
content = message_data.get("content", {})
131133

132134
if msg_type == "stream":
133-
output_lines.append(content.get("text", ""))
134-
elif msg_type == "execute_result" or msg_type == "display_data":
135-
output_lines.append(content.get("data", {}).get("text/plain", ""))
135+
stream_text = content.get("text", "")
136+
final_output_lines.append(stream_text)
137+
# --- THIS IS THE CORRECTED LINE ---
138+
await ctx.report_progress(progress=stream_text)
139+
140+
elif msg_type in ["execute_result", "display_data"]:
141+
final_output_lines.append(content.get("data", {}).get("text/plain", ""))
136142
elif msg_type == "error":
137143
error_traceback = "\n".join(content.get("traceback", []))
138144
logger.error(f"Execution error for msg_id {sent_msg_id}:\n{error_traceback}")
139145
return f"Execution Error:\n{error_traceback}"
146+
140147
elif msg_type == "status" and content.get("execution_state") == "idle":
141-
# The kernel is idle, meaning our execution is finished.
142148
execution_complete = True
143149

144150
if not execution_complete:
151+
timeout_msg = f"Error: Execution timed out after {loop_timeout} seconds."
145152
logger.error(f"Execution timed out for msg_id: {sent_msg_id}")
146-
return f"Error: Execution timed out after {loop_timeout} seconds."
153+
return timeout_msg
147154

148-
return "".join(output_lines) if output_lines else "[Execution successful with no output]"
155+
return "".join(final_output_lines) if final_output_lines else "[Execution successful with no output]"
149156

150157
except websockets.exceptions.ConnectionClosed as e:
158+
error_msg = f"Error: Could not connect to the Jupyter kernel. It may be offline. Details: {e}"
151159
logger.error(f"WebSocket connection failed: {e}")
152-
return f"Error: Could not connect to the Jupyter kernel. It may be offline. Details: {e}"
160+
return error_msg
153161
except Exception as e:
154162
logger.error(f"An unexpected error occurred during execution: {e}", exc_info=True)
155163
return f"Error: An internal server error occurred: {str(e)}"
156164

165+
@mcp.tool()
166+
async def navigate_and_get_all_visible_text(url: str) -> str:
167+
"""
168+
Retrieves all visible text from the entire webpage using Playwright.
169+
170+
Args:
171+
url: The URL of the webpage from which to retrieve text.
172+
"""
173+
# This function doesn't have intermediate steps, so it only needs 'return'.
174+
try:
175+
# Note: 'async with async_playwright() as p:' can be slow.
176+
# For performance, consider managing a single Playwright instance
177+
# outside the tool function if this tool is called frequently.
178+
async with async_playwright() as p:
179+
browser = await p.chromium.connect(PLAYWRIGHT_WS_URL)
180+
page = await browser.new_page()
181+
await page.goto(url)
182+
183+
html_content = await page.content()
184+
soup = BeautifulSoup(html_content, 'html.parser')
185+
visible_text = soup.get_text(separator="\n", strip=True)
186+
187+
await browser.close()
188+
189+
# The operation is complete, return the final result.
190+
return visible_text
191+
192+
except Exception as e:
193+
logger.error(f"Failed to retrieve all visible text: {e}")
194+
# An error occurred, return the final error message.
195+
return f"Error: Failed to retrieve all visible text: {str(e)}"
196+
157197

158-
app = mcp.sse_app()
198+
# Use the streamable_http_app as it's the modern standard
199+
app = mcp.streamable_http_app()

0 commit comments

Comments
 (0)