diff --git a/.gitignore b/.gitignore index b25c2d8..7ebadbd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,19 @@ $OPENSHIFT_DATA_DIR/ .vscode target/ +# Python +venv/ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +*.so +*.egg +*.egg-info/ +dist/ +build/ + +# MCP Selenium Server +mcp-selenium-server/screenshots/ + diff --git a/mcp-selenium-server/README.md b/mcp-selenium-server/README.md new file mode 100644 index 0000000..18d7a2e --- /dev/null +++ b/mcp-selenium-server/README.md @@ -0,0 +1,364 @@ +# MCP Selenium Server + +A Model Context Protocol (MCP) Server that exposes Selenium WebDriver tools for web browser automation. This server allows AI clients to execute standardized browser actions for automated web testing. + +## Descripción del Proyecto + +El MCP Selenium Server actúa como el "cerebro" del sistema de automatización, proporcionando una interfaz estandarizada para que los clientes AI puedan ejecutar acciones de navegador mediante Selenium WebDriver. Este servidor implementa el patrón FastMCP y se integra perfectamente con el proyecto MAT (Mentor & Automation Tool). + +## Características + +- ✅ 10 herramientas (tools) de Selenium completamente funcionales +- ✅ Soporte para Chrome y Firefox +- ✅ Integración con Selenium Grid +- ✅ Gestión automática de WebDrivers +- ✅ Capturas de pantalla automáticas +- ✅ Manejo robusto de errores + +## Requisitos Previos + +### Software Requerido + +- **Python 3.8+** +- **pip** (gestor de paquetes de Python) + +### Opcional (para Selenium Grid) + +- **Docker** y **docker-compose** (para ejecutar Selenium Grid) +- Selenium Grid configurado (ver sección de Selenium Grid más abajo) + +## Instalación + +1. Navegar al directorio del servidor: + +```bash +cd mcp-selenium-server +``` + +2. Crear un entorno virtual (recomendado): + +```bash +python3 -m venv venv +source venv/bin/activate # En Windows: venv\Scripts\activate +``` + +3. Instalar dependencias: + +```bash +pip install -r requirements.txt +``` + +## Ejecución + +### Modo Estándar (Navegador Local) + +Para ejecutar el servidor MCP con un navegador local: + +```bash +python server.py +``` + +### Modo con Selenium Grid + +Si deseas usar Selenium Grid (recomendado para pruebas distribuidas), primero inicia el Grid: + +```bash +# Desde el directorio raíz del proyecto +docker-compose -f docker-compose-v3.yml up -d --scale chrome=3 --scale firefox=3 --scale edge=1 +``` + +Luego, al usar las herramientas del servidor, especifica el `grid_url`: + +```python +start_browser(browser_type="chrome", grid_url="http://localhost:4444") +``` + +## Herramientas Disponibles + +### 1. Gestión del Navegador + +#### `start_browser` +Inicia un navegador web (Chrome o Firefox). + +**Parámetros:** +- `browser_type` (string): Tipo de navegador - "chrome" o "firefox" (por defecto: "chrome") +- `grid_url` (string, opcional): URL de Selenium Grid (ej: "http://localhost:4444") + +**Ejemplo:** +```python +# Navegador local +start_browser(browser_type="chrome") + +# Con Selenium Grid +start_browser(browser_type="firefox", grid_url="http://localhost:4444") +``` + +**Retorna:** Confirmación de inicio exitoso del navegador + +--- + +#### `close_browser` +Cierra el navegador actual. + +**Ejemplo:** +```python +close_browser() +``` + +**Retorna:** Confirmación de cierre del navegador + +--- + +### 2. Navegación + +#### `navigate_to` +Navega a una URL específica. + +**Parámetros:** +- `url` (string): La URL a visitar + +**Ejemplo:** +```python +navigate_to(url="https://www.example.com") +``` + +**Retorna:** Confirmación con la URL actual + +--- + +#### `get_page_title` +Obtiene el título de la página actual. + +**Ejemplo:** +```python +get_page_title() +``` + +**Retorna:** El título de la página + +--- + +### 3. Búsqueda y Manipulación de Elementos + +#### `find_element` +Busca un elemento en la página. + +**Parámetros:** +- `selector` (string): El selector para encontrar el elemento +- `by` (string): Tipo de selector - "id", "css", "xpath", "name", "class" (por defecto: "css") + +**Ejemplo:** +```python +find_element(selector="#search-box", by="css") +find_element(selector="//input[@name='q']", by="xpath") +``` + +**Retorna:** Información del elemento encontrado (tag, visibilidad, estado, texto) + +--- + +#### `click_element` +Hace clic en un elemento. + +**Parámetros:** +- `selector` (string): El selector para encontrar el elemento +- `by` (string): Tipo de selector - "id", "css", "xpath", "name", "class" (por defecto: "css") + +**Ejemplo:** +```python +click_element(selector="#submit-button", by="id") +click_element(selector="//button[@type='submit']", by="xpath") +``` + +**Retorna:** Confirmación de clic exitoso + +--- + +#### `input_text` +Escribe texto en un campo de entrada. + +**Parámetros:** +- `selector` (string): El selector para encontrar el elemento +- `text` (string): El texto a ingresar +- `by` (string): Tipo de selector - "id", "css", "xpath", "name", "class" (por defecto: "css") + +**Ejemplo:** +```python +input_text(selector="#username", text="testuser", by="id") +input_text(selector="//input[@name='password']", text="secret123", by="xpath") +``` + +**Retorna:** Confirmación de texto ingresado + +--- + +### 4. Verificación + +#### `element_exists` +Verifica si un elemento existe en la página. + +**Parámetros:** +- `selector` (string): El selector para encontrar el elemento +- `by` (string): Tipo de selector - "id", "css", "xpath", "name", "class" (por defecto: "css") + +**Ejemplo:** +```python +element_exists(selector=".error-message", by="css") +``` + +**Retorna:** Boolean indicando si el elemento existe y cantidad de elementos encontrados + +--- + +#### `get_element_text` +Obtiene el texto de un elemento. + +**Parámetros:** +- `selector` (string): El selector para encontrar el elemento +- `by` (string): Tipo de selector - "id", "css", "xpath", "name", "class" (por defecto: "css") + +**Ejemplo:** +```python +get_element_text(selector="#message", by="id") +get_element_text(selector="//div[@class='title']", by="xpath") +``` + +**Retorna:** El contenido de texto del elemento + +--- + +#### `take_screenshot` +Captura una screenshot de la página actual. + +**Parámetros:** +- `filename` (string, opcional): Nombre del archivo (se genera automáticamente si no se proporciona) + +**Ejemplo:** +```python +take_screenshot() +take_screenshot(filename="login_page.png") +``` + +**Retorna:** Ruta del archivo de screenshot guardado + +--- + +## Ejemplo de Uso Completo + +```python +# 1. Iniciar navegador +start_browser(browser_type="chrome") + +# 2. Navegar a un sitio web +navigate_to(url="https://www.example.com") + +# 3. Verificar título de la página +get_page_title() + +# 4. Buscar elemento +find_element(selector="#search-box", by="id") + +# 5. Ingresar texto +input_text(selector="#search-box", text="selenium automation", by="id") + +# 6. Hacer clic en botón de búsqueda +click_element(selector="#search-button", by="id") + +# 7. Verificar que existen resultados +element_exists(selector=".search-results", by="css") + +# 8. Obtener texto de un resultado +get_element_text(selector=".result-title", by="css") + +# 9. Capturar screenshot +take_screenshot(filename="search_results.png") + +# 10. Cerrar navegador +close_browser() +``` + +## Integración con el Proyecto MAT + +Este MCP Selenium Server se integra con el proyecto MAT como parte del **MAT Context** (Recursos y Herramientas Externas). Según la arquitectura definida en `doc.md`: + +- **AI Client**: Envía comandos al MCP Server +- **MCP Server**: Procesa los comandos y orquesta las acciones con el Selenium Server +- **Selenium MCP Server**: Ejecuta las acciones de automatización web + +### Flujo de Integración + +1. El usuario interactúa con el AI Client (ej: "crear pruebas automáticas") +2. El AI Client envía la solicitud al MCP Server MAT +3. El MCP Server MAT utiliza este Selenium Server para ejecutar acciones de navegador +4. Los resultados se devuelven al usuario a través del AI Client + +## Selenium Grid - Configuración + +Para usar Selenium Grid con este servidor, utiliza el archivo `docker-compose-v3.yml` del proyecto principal: + +```bash +# Iniciar Selenium Grid con múltiples nodos +docker-compose -f docker-compose-v3.yml up -d --scale chrome=3 --scale firefox=3 --scale edge=1 + +# Verificar que el Grid está funcionando +curl http://localhost:4444/status + +# Detener el Grid +docker-compose -f docker-compose-v3.yml down +``` + +El Grid estará disponible en `http://localhost:4444` y podrás usar esta URL al iniciar el navegador con `start_browser()`. + +## Estructura de Archivos + +``` +mcp-selenium-server/ +├── server.py # Servidor MCP principal con todas las herramientas +├── requirements.txt # Dependencias del proyecto +├── README.md # Esta documentación +└── screenshots/ # Directorio para capturas de pantalla (se crea automáticamente) +``` + +## Mejores Prácticas MCP + +Este servidor sigue las mejores prácticas del Model Context Protocol: + +1. ✅ **Descripción clara de herramientas**: Cada tool tiene documentación completa +2. ✅ **Tipado de parámetros**: Todos los parámetros están correctamente tipados +3. ✅ **Manejo de errores**: Captura y retorna errores de forma descriptiva +4. ✅ **Estado gestionado**: Mantiene el estado del navegador de forma segura +5. ✅ **Respuestas informativas**: Todas las operaciones retornan mensajes claros + +## Solución de Problemas + +### Error: "Browser is not started" +**Solución:** Ejecuta `start_browser()` antes de cualquier otra operación. + +### Error: "Element not found (timeout)" +**Solución:** Verifica que el selector sea correcto y que el elemento exista en la página. Los timeouts por defecto son de 10 segundos. + +### Error al instalar dependencias +**Solución:** Asegúrate de tener Python 3.8+ y pip actualizado: +```bash +python3 --version +pip install --upgrade pip +``` + +### Problemas con ChromeDriver/GeckoDriver +**Solución:** El paquete `webdriver-manager` se encarga de descargar e instalar los drivers automáticamente. Si hay problemas, verifica tu conexión a internet. + +## Contribuciones + +Este proyecto forma parte del repositorio [juananmora/seleniumchatgpt4](https://github.com/juananmora/seleniumchatgpt4). Las contribuciones son bienvenidas siguiendo las guías del proyecto principal. + +## Licencia + +Este proyecto está bajo la licencia Apache License 2.0, al igual que el proyecto principal. + +## Referencias + +- [Documentación del proyecto MAT](../doc.md) +- [README principal del proyecto](../README.md) +- [Selenium Documentation](https://www.selenium.dev/documentation/) +- [Model Context Protocol](https://modelcontextprotocol.io/) +- [FastMCP Documentation](https://github.com/jlowin/fastmcp) diff --git a/mcp-selenium-server/requirements.txt b/mcp-selenium-server/requirements.txt new file mode 100644 index 0000000..006f37d --- /dev/null +++ b/mcp-selenium-server/requirements.txt @@ -0,0 +1,3 @@ +mcp>=1.23.0 +selenium +webdriver-manager diff --git a/mcp-selenium-server/server.py b/mcp-selenium-server/server.py new file mode 100644 index 0000000..a19117c --- /dev/null +++ b/mcp-selenium-server/server.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 +""" +MCP Selenium Server - Model Context Protocol Server for Selenium automation +Provides standardized tools for web browser automation using Selenium WebDriver +""" + +from mcp.server.fastmcp import FastMCP +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException +from selenium.webdriver.chrome.service import Service as ChromeService +from selenium.webdriver.firefox.service import Service as FirefoxService +from webdriver_manager.chrome import ChromeDriverManager +from webdriver_manager.firefox import GeckoDriverManager +import os +from datetime import datetime + +# Initialize FastMCP server +mcp = FastMCP("Selenium MCP Server") + +# Global variable to store the WebDriver instance +driver = None + +def get_by_type(by: str) -> By: + """Convert string selector type to Selenium By type""" + by_mapping = { + "id": By.ID, + "css": By.CSS_SELECTOR, + "xpath": By.XPATH, + "name": By.NAME, + "class": By.CLASS_NAME, + "tag": By.TAG_NAME, + "link_text": By.LINK_TEXT, + "partial_link_text": By.PARTIAL_LINK_TEXT + } + return by_mapping.get(by.lower(), By.CSS_SELECTOR) + +@mcp.tool() +def start_browser(browser_type: str = "chrome", grid_url: str = None) -> str: + """ + Initialize a web browser (Chrome or Firefox) + + Args: + browser_type: Type of browser to start ("chrome" or "firefox") + grid_url: Optional URL for Selenium Grid connection (e.g., "http://localhost:4444") + + Returns: + Confirmation message with browser type started + """ + global driver + + if driver is not None: + return "Browser is already running. Please close it first before starting a new one." + + try: + browser_type = browser_type.lower() + + if grid_url: + # Connect to Selenium Grid + if browser_type == "chrome": + options = webdriver.ChromeOptions() + driver = webdriver.Remote( + command_executor=grid_url, + options=options + ) + elif browser_type == "firefox": + options = webdriver.FirefoxOptions() + driver = webdriver.Remote( + command_executor=grid_url, + options=options + ) + else: + return f"Unsupported browser type: {browser_type}. Use 'chrome' or 'firefox'." + return f"{browser_type.capitalize()} browser started successfully using Selenium Grid at {grid_url}" + else: + # Start local browser + if browser_type == "chrome": + service = ChromeService(ChromeDriverManager().install()) + driver = webdriver.Chrome(service=service) + elif browser_type == "firefox": + service = FirefoxService(GeckoDriverManager().install()) + driver = webdriver.Firefox(service=service) + else: + return f"Unsupported browser type: {browser_type}. Use 'chrome' or 'firefox'." + + return f"{browser_type.capitalize()} browser started successfully" + except Exception as e: + driver = None + return f"Error starting browser: {str(e)}" + +@mcp.tool() +def close_browser() -> str: + """ + Close the current browser session + + Returns: + Confirmation message of browser closure + """ + global driver + + if driver is None: + return "No browser is currently running" + + try: + driver.quit() + driver = None + return "Browser closed successfully" + except Exception as e: + driver = None + return f"Error closing browser: {str(e)}" + +@mcp.tool() +def navigate_to(url: str) -> str: + """ + Navigate to a specific URL + + Args: + url: The URL to navigate to + + Returns: + Confirmation message with the current URL + """ + global driver + + if driver is None: + return "Browser is not started. Please start the browser first using start_browser()." + + try: + driver.get(url) + return f"Successfully navigated to: {driver.current_url}" + except Exception as e: + return f"Error navigating to URL: {str(e)}" + +@mcp.tool() +def find_element(selector: str, by: str = "css") -> str: + """ + Find an element on the page by selector + + Args: + selector: The selector string to find the element + by: The type of selector ("id", "css", "xpath", "name", "class") + + Returns: + Information about the found element or error message + """ + global driver + + if driver is None: + return "Browser is not started. Please start the browser first using start_browser()." + + try: + by_type = get_by_type(by) + wait = WebDriverWait(driver, 10) + element = wait.until(EC.presence_of_element_located((by_type, selector))) + + tag_name = element.tag_name + element_text = element.text[:100] if element.text else "" + is_displayed = element.is_displayed() + is_enabled = element.is_enabled() + + return f"Element found - Tag: {tag_name}, Displayed: {is_displayed}, Enabled: {is_enabled}, Text: '{element_text}'" + except TimeoutException: + return f"Element not found with {by}='{selector}' (timeout)" + except Exception as e: + return f"Error finding element: {str(e)}" + +@mcp.tool() +def click_element(selector: str, by: str = "css") -> str: + """ + Click on an element + + Args: + selector: The selector string to find the element + by: The type of selector ("id", "css", "xpath", "name", "class") + + Returns: + Confirmation message of successful click + """ + global driver + + if driver is None: + return "Browser is not started. Please start the browser first using start_browser()." + + try: + by_type = get_by_type(by) + wait = WebDriverWait(driver, 10) + element = wait.until(EC.element_to_be_clickable((by_type, selector))) + element.click() + return f"Successfully clicked element with {by}='{selector}'" + except TimeoutException: + return f"Element not clickable with {by}='{selector}' (timeout)" + except Exception as e: + return f"Error clicking element: {str(e)}" + +@mcp.tool() +def input_text(selector: str, text: str, by: str = "css") -> str: + """ + Input text into a form field + + Args: + selector: The selector string to find the input element + text: The text to input + by: The type of selector ("id", "css", "xpath", "name", "class") + + Returns: + Confirmation message of text input + """ + global driver + + if driver is None: + return "Browser is not started. Please start the browser first using start_browser()." + + try: + by_type = get_by_type(by) + wait = WebDriverWait(driver, 10) + element = wait.until(EC.presence_of_element_located((by_type, selector))) + element.clear() + element.send_keys(text) + return f"Successfully input text into element with {by}='{selector}'" + except TimeoutException: + return f"Element not found with {by}='{selector}' (timeout)" + except Exception as e: + return f"Error inputting text: {str(e)}" + +@mcp.tool() +def get_page_title() -> str: + """ + Get the title of the current page + + Returns: + The page title or error message + """ + global driver + + if driver is None: + return "Browser is not started. Please start the browser first using start_browser()." + + try: + title = driver.title + return f"Page title: {title}" + except Exception as e: + return f"Error getting page title: {str(e)}" + +@mcp.tool() +def element_exists(selector: str, by: str = "css") -> str: + """ + Check if an element exists on the page + + Args: + selector: The selector string to find the element + by: The type of selector ("id", "css", "xpath", "name", "class") + + Returns: + Boolean result indicating if element exists + """ + global driver + + if driver is None: + return "Browser is not started. Please start the browser first using start_browser()." + + try: + by_type = get_by_type(by) + elements = driver.find_elements(by_type, selector) + exists = len(elements) > 0 + return f"Element exists: {exists} (found {len(elements)} element(s))" + except Exception as e: + return f"Error checking element existence: {str(e)}" + +@mcp.tool() +def get_element_text(selector: str, by: str = "css") -> str: + """ + Get the text content of an element + + Args: + selector: The selector string to find the element + by: The type of selector ("id", "css", "xpath", "name", "class") + + Returns: + The text content of the element + """ + global driver + + if driver is None: + return "Browser is not started. Please start the browser first using start_browser()." + + try: + by_type = get_by_type(by) + wait = WebDriverWait(driver, 10) + element = wait.until(EC.presence_of_element_located((by_type, selector))) + text = element.text + return f"Element text: {text}" + except TimeoutException: + return f"Element not found with {by}='{selector}' (timeout)" + except Exception as e: + return f"Error getting element text: {str(e)}" + +@mcp.tool() +def take_screenshot(filename: str = None) -> str: + """ + Take a screenshot of the current page + + Args: + filename: Optional filename for the screenshot (defaults to timestamp-based name) + + Returns: + Path to the saved screenshot file + """ + global driver + + if driver is None: + return "Browser is not started. Please start the browser first using start_browser()." + + try: + # Create screenshots directory if it doesn't exist + screenshots_dir = os.path.join(os.path.dirname(__file__), "screenshots") + os.makedirs(screenshots_dir, exist_ok=True) + + # Generate filename if not provided + if filename is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"screenshot_{timestamp}.png" + + # Ensure .png extension + if not filename.endswith('.png'): + filename += '.png' + + filepath = os.path.join(screenshots_dir, filename) + driver.save_screenshot(filepath) + + return f"Screenshot saved to: {filepath}" + except Exception as e: + return f"Error taking screenshot: {str(e)}" + +if __name__ == "__main__": + # Run the MCP server + mcp.run()