#
tokens: 6319/50000 4/4 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── changelog.md
├── LICENSE
├── pyproject.toml
├── README.md
└── src
    └── mcp_web_browser
        └── server.py
```

# Files

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Web Browser Server
  2 | 
  3 | An advanced web browsing server for the Model Context Protocol (MCP) powered by Playwright, enabling headless browser interactions through a flexible, secure API.
  4 | 
  5 | <a href="https://glama.ai/mcp/servers/lwqlaw6k6d"><img width="380" height="200" src="https://glama.ai/mcp/servers/lwqlaw6k6d/badge" alt="Web Browser Server MCP server" /></a>
  6 | 
  7 | ## 🌐 Features
  8 | 
  9 | - **Headless Web Browsing**: Navigate to any website with SSL certificate validation bypass
 10 | - **Full Page Content Extraction**: Retrieve complete HTML content, including dynamically loaded JavaScript
 11 | - **Multi-Tab Support**: Create, manage, and switch between multiple browser tabs
 12 | - **Advanced Web Interaction Tools**:
 13 |   - Extract text content
 14 |   - Click page elements
 15 |   - Input text into form fields
 16 |   - Capture screenshots
 17 |   - Extract page links with filtering capabilities
 18 |   - Scroll pages in any direction
 19 |   - Execute JavaScript on pages
 20 |   - Refresh pages
 21 |   - Wait for navigation to complete
 22 | - **Resource Management**: Automatic cleanup of unused resources after inactivity
 23 | - **Enhanced Page Information**: Get detailed metadata about the current page
 24 | 
 25 | ## 🚀 Quick Start
 26 | 
 27 | ### Prerequisites
 28 | 
 29 | - Python 3.10+
 30 | - MCP SDK
 31 | - Playwright
 32 | 
 33 | ### Installation
 34 | 
 35 | ```bash
 36 | # Install MCP and Playwright
 37 | pip install mcp playwright
 38 | 
 39 | # Install browser dependencies
 40 | playwright install
 41 | ```
 42 | 
 43 | ### Configuration for Claude Desktop
 44 | 
 45 | Add to your `claude_desktop_config.json`:
 46 | 
 47 | ```json
 48 | {
 49 |   "mcpServers": {
 50 |     "web-browser": {
 51 |       "command": "python",
 52 |       "args": [
 53 |         "/path/to/your/server.py"
 54 |       ]
 55 |     }
 56 |   }
 57 | }
 58 | ```
 59 | 
 60 | ## 💡 Usage Examples
 61 | 
 62 | ### Basic Web Navigation
 63 | 
 64 | ```python
 65 | # Browse to a website
 66 | page_content = browse_to("https://example.com")
 67 | 
 68 | # Extract page text
 69 | text_content = extract_text_content()
 70 | 
 71 | # Extract text from a specific element
 72 | title_text = extract_text_content("h1.title")
 73 | ```
 74 | 
 75 | ### Web Interaction
 76 | 
 77 | ```python
 78 | # Navigate to a page
 79 | browse_to("https://example.com/login")
 80 | 
 81 | # Input text into a form
 82 | input_text("#username", "your_username")
 83 | input_text("#password", "your_password")
 84 | 
 85 | # Click a login button
 86 | click_element("#login-button")
 87 | ```
 88 | 
 89 | ### Screenshot Capture
 90 | 
 91 | ```python
 92 | # Capture full page screenshot
 93 | full_page_screenshot = get_page_screenshots(full_page=True)
 94 | 
 95 | # Capture specific element screenshot
 96 | element_screenshot = get_page_screenshots(selector="#main-content")
 97 | ```
 98 | 
 99 | ### Link Extraction
100 | 
101 | ```python
102 | # Get all links on the page
103 | page_links = get_page_links()
104 | 
105 | # Get links matching a pattern
106 | filtered_links = get_page_links(filter_pattern="contact")
107 | ```
108 | 
109 | ### Multi-Tab Browsing
110 | 
111 | ```python
112 | # Create a new tab
113 | tab_id = create_new_tab("https://example.com")
114 | 
115 | # Create another tab
116 | another_tab_id = create_new_tab("https://example.org")
117 | 
118 | # List all open tabs
119 | tabs = list_tabs()
120 | 
121 | # Switch between tabs
122 | switch_tab(tab_id)
123 | 
124 | # Close a tab
125 | close_tab(another_tab_id)
126 | ```
127 | 
128 | ### Advanced Interactions
129 | 
130 | ```python
131 | # Scroll the page
132 | scroll_page(direction="down", amount="page")
133 | 
134 | # Execute JavaScript on the page
135 | result = execute_javascript("return document.title")
136 | 
137 | # Get detailed page information
138 | page_info = get_page_info()
139 | 
140 | # Refresh the current page
141 | refresh_page()
142 | 
143 | # Wait for navigation to complete
144 | wait_for_navigation(timeout_ms=5000)
145 | ```
146 | 
147 | ## 🛡️ Security Features
148 | 
149 | - SSL certificate validation bypass
150 | - Secure browser context management
151 | - Custom user-agent configuration
152 | - Error handling and comprehensive logging
153 | - Configurable timeout settings
154 | - CSP bypass control
155 | - Protection against cookie stealing
156 | 
157 | ## 🔧 Troubleshooting
158 | 
159 | ### Common Issues
160 | 
161 | - **SSL Certificate Errors**: Automatically bypassed
162 | - **Slow Page Load**: Adjust timeout in `browse_to()` method
163 | - **Element Not Found**: Verify selectors carefully
164 | - **Browser Resource Usage**: Auto-cleanup after inactivity period
165 | 
166 | ### Logging
167 | 
168 | All significant events are logged with detailed information for easy debugging.
169 | 
170 | ## 📋 Tool Parameters
171 | 
172 | ### `browse_to(url: str, context: Optional[Any] = None)`
173 | - `url`: Website to navigate to
174 | - `context`: Optional context object (currently unused)
175 | 
176 | ### `extract_text_content(selector: Optional[str] = None, context: Optional[Any] = None)`
177 | - `selector`: Optional CSS selector to extract specific content
178 | - `context`: Optional context object (currently unused)
179 | 
180 | ### `click_element(selector: str, context: Optional[Any] = None)`
181 | - `selector`: CSS selector of the element to click
182 | - `context`: Optional context object (currently unused)
183 | 
184 | ### `get_page_screenshots(full_page: bool = False, selector: Optional[str] = None, context: Optional[Any] = None)`
185 | - `full_page`: Capture entire page screenshot
186 | - `selector`: Optional element to screenshot
187 | - `context`: Optional context object (currently unused)
188 | 
189 | ### `get_page_links(filter_pattern: Optional[str] = None, context: Optional[Any] = None)`
190 | - `filter_pattern`: Optional text pattern to filter links
191 | - `context`: Optional context object (currently unused)
192 | 
193 | ### `input_text(selector: str, text: str, context: Optional[Any] = None)`
194 | - `selector`: CSS selector of input element
195 | - `text`: Text to input
196 | - `context`: Optional context object (currently unused)
197 | 
198 | ### `create_new_tab(url: Optional[str] = None, context: Optional[Any] = None)`
199 | - `url`: Optional URL to navigate to in the new tab
200 | - `context`: Optional context object (currently unused)
201 | 
202 | ### `switch_tab(tab_id: str, context: Optional[Any] = None)`
203 | - `tab_id`: ID of the tab to switch to
204 | - `context`: Optional context object (currently unused)
205 | 
206 | ### `list_tabs(context: Optional[Any] = None)`
207 | - `context`: Optional context object (currently unused)
208 | 
209 | ### `close_tab(tab_id: Optional[str] = None, context: Optional[Any] = None)`
210 | - `tab_id`: Optional ID of the tab to close (defaults to current tab)
211 | - `context`: Optional context object (currently unused)
212 | 
213 | ### `refresh_page(context: Optional[Any] = None)`
214 | - `context`: Optional context object (currently unused)
215 | 
216 | ### `get_page_info(context: Optional[Any] = None)`
217 | - `context`: Optional context object (currently unused)
218 | 
219 | ### `scroll_page(direction: str = "down", amount: str = "page", context: Optional[Any] = None)`
220 | - `direction`: Direction to scroll ('up', 'down', 'left', 'right')
221 | - `amount`: Amount to scroll ('page', 'half', or a number)
222 | - `context`: Optional context object (currently unused)
223 | 
224 | ### `wait_for_navigation(timeout_ms: int = 10000, context: Optional[Any] = None)`
225 | - `timeout_ms`: Maximum time to wait in milliseconds
226 | - `context`: Optional context object (currently unused)
227 | 
228 | ### `execute_javascript(script: str, context: Optional[Any] = None)`
229 | - `script`: JavaScript code to execute
230 | - `context`: Optional context object (currently unused)
231 | 
232 | ## 🤝 Contributing
233 | 
234 | Contributions are welcome! Please feel free to submit a Pull Request.
235 | 
236 | ### Development Setup
237 | 
238 | ```bash
239 | # Clone the repository
240 | git clone https://github.com/random-robbie/mcp-web-browser.git
241 | 
242 | # Create virtual environment
243 | python -m venv venv
244 | source venv/bin/activate  # On Windows use `venv\Scripts\activate`
245 | 
246 | # Install dependencies
247 | pip install -e .[dev]
248 | ```
249 | 
250 | ## 📄 License
251 | 
252 | MIT License
253 | 
254 | ## 🔗 Related Projects
255 | 
256 | - [Model Context Protocol](https://modelcontextprotocol.io)
257 | - [Playwright](https://playwright.dev)
258 | - [Claude Desktop](https://claude.ai/desktop)
259 | 
260 | ## 💬 Support
261 | 
262 | For issues and questions, please [open an issue](https://github.com/random-robbie/mcp-web-browser/issues) on GitHub.
263 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [build-system]
 2 | requires = ["hatchling"]
 3 | build-backend = "hatchling.build"
 4 | 
 5 | [project]
 6 | name = "mcp-web-browser"
 7 | version = "0.1.0"
 8 | description = "MCP Web Browser Server using Playwright"
 9 | readme = "README.md"
10 | requires-python = ">=3.10"
11 | dependencies = [
12 |     "mcp[cli]",
13 |     "playwright",
14 | ]
15 | 
16 | [project.optional-dependencies]
17 | dev = [
18 |     "pytest",
19 |     "ruff",
20 | ]
21 | 
22 | [tool.hatch.build.targets.wheel]
23 | packages = ["src/mcp_web_browser"]
24 | 
25 | [tool.hatch.envs.default]
26 | dependencies = [
27 |     "pytest",
28 |     "ruff",
29 | ]
30 | 
31 | [tool.ruff]
32 | line-length = 100
33 | target-version = "py310"
34 | 
35 | [tool.ruff.lint]
36 | select = ["E", "F", "W", "I", "N", "UP", "ASYNC"]
37 | ignore = ["E501"]
38 | 
39 | [project.scripts]
40 | mcp-web-browser = "mcp_web_browser.server:main"
41 | 
```

--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------

```markdown
 1 | Security Enhancements
 2 | 
 3 | Proper Error Handling and Logging: Added comprehensive logging with formatted output and proper error tracing
 4 | URL Sanitization: Automatically adds HTTPS prefix to URLs when needed
 5 | Content Security Policy Bypass Control: Added option to bypass CSP for better testing capabilities
 6 | Custom User-Agent: Configurable user-agent string
 7 | Security Headers: Added security-related HTTP headers to requests
 8 | JavaScript Execution Restrictions: Added validation to prevent cookie stealing via JavaScript
 9 | 
10 | Performance Improvements
11 | 
12 | Browser Inactivity Timeout: Automatically closes browser after inactivity to free resources
13 | Optimized Page Loading: Changed from 'networkidle' to 'domcontentloaded' for faster page loading
14 | Resource Cleanup: Improved cleanup processes to ensure all resources are properly released
15 | Retry Logic: Added retry mechanisms for page creation and element clicking
16 | Efficient Screenshot Capture: Added JPEG compression for faster screenshot processing
17 | 
18 | Functional Additions
19 | 
20 | Multi-Tab Support: Added ability to create, switch between, and manage multiple tabs
21 | 
22 | create_new_tab: Opens a new browser tab
23 | switch_tab: Switches between open tabs
24 | list_tabs: Shows all open tabs and their information
25 | close_tab: Closes a specific tab
26 | 
27 | 
28 | Advanced Page Interaction:
29 | 
30 | refresh_page: Refreshes the current page
31 | get_page_info: Provides detailed page information including metadata
32 | scroll_page: Controls page scrolling in all directions
33 | wait_for_navigation: Waits for page navigation to complete
34 | execute_javascript: Safely executes JavaScript on the page
35 | 
36 | 
37 | Enhanced Link Extraction:
38 | 
39 | Added filtering capabilities for links
40 | Returns more metadata about links (title, text, etc.)
41 | 
42 | 
43 | Improved Element Interaction:
44 | 
45 | Better visibility checking for elements
46 | Automatic scrolling to ensure elements are in view
47 | More reliable clicking with retry logic
48 | 
49 | 
50 | 
51 | Code Structure and Reliability
52 | 
53 | Better State Management: Added global state tracking for browser, pages, and tabs
54 | Background Monitoring: Added inactivity monitor to clean up unused resources
55 | Detailed Logging: Comprehensive logging throughout the system
56 | Dynamic Playwright Import: Better error handling for Playwright import
57 | Configurability: Added more configuration options for timeouts, viewport size, etc.
58 | 
59 | These improvements make the web browser MCP server more robust, secure, and feature-rich for your pentesting work, while ensuring resources are properly managed.
60 | 
```

--------------------------------------------------------------------------------
/src/mcp_web_browser/server.py:
--------------------------------------------------------------------------------

```python
  1 | from typing import Optional, Union, Any
  2 | import asyncio
  3 | import base64
  4 | import sys
  5 | from mcp.server import FastMCP
  6 | 
  7 | # Create an MCP server for web browsing
  8 | mcp = FastMCP("Web Browser")
  9 | 
 10 | # Global browser and page management
 11 | _browser = None
 12 | _browser_context = None
 13 | _current_page = None
 14 | _playwright_instance = None
 15 | 
 16 | # Dynamic import of Playwright to avoid early import errors
 17 | def _import_playwright():
 18 |     from playwright.async_api import async_playwright
 19 |     return async_playwright
 20 | 
 21 | async def _ensure_browser():
 22 |     """Ensure a browser instance is available with SSL validation disabled"""
 23 |     global _browser, _browser_context, _playwright_instance
 24 |     
 25 |     if _browser is None:
 26 |         playwright_module = _import_playwright()
 27 |         _playwright_instance = await playwright_module().start()
 28 |         _browser = await _playwright_instance.chromium.launch()
 29 |         
 30 |         # Create a browser context that ignores HTTPS errors
 31 |         _browser_context = await _browser.new_context(
 32 |             ignore_https_errors=True,  # Ignore SSL certificate errors
 33 |         )
 34 |     return _browser, _browser_context
 35 | 
 36 | async def _close_current_page():
 37 |     """Close the current page if it exists"""
 38 |     global _current_page
 39 |     if _current_page:
 40 |         try:
 41 |             await _current_page.close()
 42 |         except Exception:
 43 |             pass
 44 |         _current_page = None
 45 | 
 46 | async def _safe_cleanup():
 47 |     """Safely clean up browser resources"""
 48 |     global _browser, _current_page, _browser_context, _playwright_instance
 49 |     
 50 |     try:
 51 |         if _current_page:
 52 |             try:
 53 |                 await _current_page.close()
 54 |             except Exception:
 55 |                 pass
 56 |         
 57 |         if _browser_context:
 58 |             try:
 59 |                 await _browser_context.close()
 60 |             except Exception:
 61 |                 pass
 62 |         
 63 |         if _browser:
 64 |             try:
 65 |                 await _browser.close()
 66 |             except Exception:
 67 |                 pass
 68 |         
 69 |         if _playwright_instance:
 70 |             try:
 71 |                 await _playwright_instance.stop()
 72 |             except Exception:
 73 |                 pass
 74 |     except Exception as e:
 75 |         # Log the error, but don't re-raise
 76 |         print(f"Error during cleanup: {e}", file=sys.stderr)
 77 |     finally:
 78 |         # Reset global variables
 79 |         _browser = None
 80 |         _browser_context = None
 81 |         _current_page = None
 82 |         _playwright_instance = None
 83 | 
 84 | @mcp.tool()
 85 | async def browse_to(url: str, context: Optional[Any] = None) -> str:
 86 |     """
 87 |     Navigate to a specific URL and return the page's HTML content.
 88 |     
 89 |     Args:
 90 |         url: The full URL to navigate to
 91 |         context: Optional context object for logging (ignored)
 92 |     
 93 |     Returns:
 94 |         The full HTML content of the page
 95 |     """
 96 |     global _current_page, _browser, _browser_context
 97 |     
 98 |     # Ensure browser is launched with SSL validation disabled
 99 |     _, browser_context = await _ensure_browser()
100 |     
101 |     # Close any existing page
102 |     await _close_current_page()
103 |     
104 |     # Optional logging, but do nothing with context
105 |     print(f"Navigating to {url}", file=sys.stderr)
106 |     
107 |     try:
108 |         # Create a new page and navigate
109 |         _current_page = await browser_context.new_page()
110 |         
111 |         # Additional options to handle various SSL/security issues
112 |         await _current_page.goto(url, 
113 |             wait_until='networkidle',
114 |             timeout=30000,  # 30 seconds timeout
115 |         )
116 |         
117 |         # Get full page content including dynamically loaded JavaScript
118 |         page_content = await _current_page.content()
119 |         
120 |         # Optional: extract additional metadata
121 |         try:
122 |             title = await _current_page.title()
123 |             print(f"Page title: {title}", file=sys.stderr)
124 |         except Exception:
125 |             pass
126 |         
127 |         return page_content
128 |     
129 |     except Exception as e:
130 |         print(f"Error navigating to {url}: {e}", file=sys.stderr)
131 |         raise
132 | 
133 | @mcp.tool()
134 | async def extract_text_content(
135 |     selector: Optional[str] = None, 
136 |     context: Optional[Any] = None
137 | ) -> str:
138 |     """
139 |     Extract text content from the current page, optionally using a CSS selector.
140 |     
141 |     Args:
142 |         selector: Optional CSS selector to target specific elements
143 |         context: Optional context object for logging (ignored)
144 |     
145 |     Returns:
146 |         Extracted text content
147 |     """
148 |     global _current_page
149 |     
150 |     if not _current_page:
151 |         raise ValueError("No page is currently loaded. Use browse_to first.")
152 |     
153 |     try:
154 |         if selector:
155 |             # If selector is provided, extract text from matching elements
156 |             elements = await _current_page.query_selector_all(selector)
157 |             text_content = "\n".join([await el.inner_text() for el in elements])
158 |             print(f"Extracted text from selector: {selector}", file=sys.stderr)
159 |         else:
160 |             # If no selector, extract all visible text from the page
161 |             text_content = await _current_page.inner_text('body')
162 |         
163 |         return text_content
164 |     
165 |     except Exception as e:
166 |         print(f"Error extracting text: {e}", file=sys.stderr)
167 |         raise ValueError(f"Error extracting text: {e}")
168 | 
169 | @mcp.tool()
170 | async def click_element(
171 |     selector: str, 
172 |     context: Optional[Any] = None
173 | ) -> str:
174 |     """
175 |     Click an element on the current page.
176 |     
177 |     Args:
178 |         selector: CSS selector for the element to click
179 |         context: Optional context object for logging (ignored)
180 |     
181 |     Returns:
182 |         Confirmation message or error details
183 |     """
184 |     global _current_page
185 |     
186 |     if not _current_page:
187 |         raise ValueError("No page is currently loaded. Use browse_to first.")
188 |     
189 |     try:
190 |         element = await _current_page.query_selector(selector)
191 |         if not element:
192 |             raise ValueError(f"No element found with selector: {selector}")
193 |         
194 |         await element.click()
195 |         print(f"Clicked element: {selector}", file=sys.stderr)
196 |         
197 |         return f"Successfully clicked element: {selector}"
198 |     
199 |     except Exception as e:
200 |         print(f"Error clicking element: {e}", file=sys.stderr)
201 |         raise ValueError(f"Error clicking element: {e}")
202 | 
203 | @mcp.tool()
204 | async def get_page_screenshots(
205 |     full_page: bool = False, 
206 |     selector: Optional[str] = None,
207 |     context: Optional[Any] = None
208 | ) -> str:
209 |     """
210 |     Capture screenshot of the current page.
211 |     
212 |     Args:
213 |         full_page: Whether to capture the entire page or just the viewport
214 |         selector: Optional CSS selector to screenshot a specific element
215 |         context: Optional context object for logging (ignored)
216 |     
217 |     Returns:
218 |         Base64 encoded screenshot image
219 |     """
220 |     global _current_page
221 |     
222 |     if not _current_page:
223 |         raise ValueError("No page is currently loaded. Use browse_to first.")
224 |     
225 |     try:
226 |         if selector:
227 |             element = await _current_page.query_selector(selector)
228 |             if not element:
229 |                 raise ValueError(f"No element found with selector: {selector}")
230 |             screenshot_bytes = await element.screenshot()
231 |         else:
232 |             screenshot_bytes = await _current_page.screenshot(full_page=full_page)
233 |         
234 |         # Convert to base64 for easy transmission
235 |         screenshot_base64 = base64.b64encode(screenshot_bytes).decode('utf-8')
236 |         
237 |         print(f"Screenshot captured: {'full page' if full_page else 'viewport'}", file=sys.stderr)
238 |         
239 |         return screenshot_base64
240 |     
241 |     except Exception as e:
242 |         print(f"Error capturing screenshot: {e}", file=sys.stderr)
243 |         raise ValueError(f"Error capturing screenshot: {e}")
244 | 
245 | @mcp.tool()
246 | async def get_page_links(context: Optional[Any] = None) -> list[str]:
247 |     """
248 |     Extract all links from the current page.
249 |     
250 |     Args:
251 |         context: Optional context object for logging (ignored)
252 |     
253 |     Returns:
254 |         List of links found on the page
255 |     """
256 |     global _current_page
257 |     
258 |     if not _current_page:
259 |         raise ValueError("No page is currently loaded. Use browse_to first.")
260 |     
261 |     try:
262 |         # Use JavaScript to extract all links
263 |         links = await _current_page.evaluate("""
264 |             () => {
265 |                 const links = document.querySelectorAll('a');
266 |                 return Array.from(links).map(link => link.href);
267 |             }
268 |         """)
269 |         
270 |         print(f"Extracted {len(links)} links from the page", file=sys.stderr)
271 |         
272 |         return links
273 |     
274 |     except Exception as e:
275 |         print(f"Error extracting links: {e}", file=sys.stderr)
276 |         raise ValueError(f"Error extracting links: {e}")
277 | 
278 | @mcp.tool()
279 | async def input_text(
280 |     selector: str, 
281 |     text: str, 
282 |     context: Optional[Any] = None
283 | ) -> str:
284 |     """
285 |     Input text into a specific element on the page.
286 |     
287 |     Args:
288 |         selector: CSS selector for the input element
289 |         text: Text to input
290 |         context: Optional context object for logging (ignored)
291 |     
292 |     Returns:
293 |         Confirmation message
294 |     """
295 |     global _current_page
296 |     
297 |     if not _current_page:
298 |         raise ValueError("No page is currently loaded. Use browse_to first.")
299 |     
300 |     try:
301 |         element = await _current_page.query_selector(selector)
302 |         if not element:
303 |             raise ValueError(f"No element found with selector: {selector}")
304 |         
305 |         await element.fill(text)
306 |         
307 |         print(f"Input text into element: {selector}", file=sys.stderr)
308 |         
309 |         return f"Successfully input text into element: {selector}"
310 |     
311 |     except Exception as e:
312 |         print(f"Error inputting text: {e}", file=sys.stderr)
313 |         raise ValueError(f"Error inputting text: {e}")
314 | 
315 | def main():
316 |     try:
317 |         mcp.run()
318 |     except Exception as e:
319 |         print(f"Error running MCP server: {e}", file=sys.stderr)
320 |     finally:
321 |         # Use a separate event loop to ensure cleanup
322 |         try:
323 |             loop = asyncio.new_event_loop()
324 |             asyncio.set_event_loop(loop)
325 |             loop.run_until_complete(_safe_cleanup())
326 |             loop.close()
327 |         except Exception as cleanup_error:
328 |             print(f"Cleanup error: {cleanup_error}", file=sys.stderr)
329 | 
330 | if __name__ == "__main__":
331 |     main()
332 | 
```