1111
1212import aiofiles
1313import 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 ---
1720logging .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