From d2ed83bb58cf4bdf12a726bc8e8f2cc05d7760ee Mon Sep 17 00:00:00 2001 From: tumberger Date: Sat, 22 Mar 2025 15:29:47 -0700 Subject: [PATCH 01/12] include mcp-proxy --- server/server.py | 121 +++++++++++++++++++++++++++++++---------------- 1 file changed, 79 insertions(+), 42 deletions(-) diff --git a/server/server.py b/server/server.py index 301e585..b12127c 100644 --- a/server/server.py +++ b/server/server.py @@ -619,6 +619,7 @@ async def read_resource(uri: str) -> list[types.ResourceContents]: default=CONFIG["DEFAULT_TASK_EXPIRY_MINUTES"], help="Minutes after which tasks are considered expired", ) +@click.option("--stdio", is_flag=True, default=False, help="Enable stdio mode with mcp-proxy") def main( port: int, chrome_path: str, @@ -626,6 +627,7 @@ def main( window_height: int, locale: str, task_expiry_minutes: int, + stdio: bool, ) -> int: """ Run the browser-use MCP server. @@ -640,6 +642,7 @@ def main( window_height: Browser window height locale: Browser locale task_expiry_minutes: Minutes after which tasks are considered expired + stdio: Enable stdio mode with mcp-proxy Returns: Exit code (0 for success) @@ -673,54 +676,88 @@ def main( sse = SseServerTransport("/messages/") - async def handle_sse(request): - """Handle SSE connections from clients.""" + if stdio: + # When stdio is enabled, we'll use mcp-proxy to create a proxy + # that runs our server and exposes stdio + import subprocess + import sys + + # Get the absolute path to the current script + server_module = sys.modules[__name__] + server_file = os.path.abspath(server_module.__file__) + + # Use sys.executable to get the current Python interpreter path + python_executable = sys.executable + + # Start mcp-proxy with our server command + proxy_cmd = [ + "mcp-proxy", + "--sse-port", str(port), + "--allow-origin", "*", + "--", # Separate mcp-proxy args from command args + python_executable, server_file, "--port", str(port) + ] + + logger.info(f"Running proxy command: {' '.join(proxy_cmd)}") + + # Run the proxy try: - async with sse.connect_sse( - request.scope, request.receive, request._send - ) as streams: - await app.run( - streams[0], streams[1], app.create_initialization_options() - ) + with subprocess.Popen(proxy_cmd) as proxy_process: + proxy_process.wait() except Exception as e: - logger.error(f"Error in handle_sse: {str(e)}") - raise - - starlette_app = Starlette( - debug=True, - routes=[ - Route("/sse", endpoint=handle_sse), - Mount("/messages/", app=sse.handle_post_message), - ], - ) + logger.error(f"Error starting mcp-proxy: {str(e)}") + logger.error(f"Command was: {' '.join(proxy_cmd)}") + return 1 + else: + # When stdio is disabled, we'll just run the SSE server directly + async def handle_sse(request): + """Handle SSE connections from clients.""" + try: + async with sse.connect_sse( + request.scope, request.receive, request._send + ) as streams: + await app.run( + streams[0], streams[1], app.create_initialization_options() + ) + except Exception as e: + logger.error(f"Error in handle_sse: {str(e)}") + raise + + starlette_app = Starlette( + debug=True, + routes=[ + Route("/sse", endpoint=handle_sse), + Mount("/messages/", app=sse.handle_post_message), + ], + ) - # Add a startup event - @starlette_app.on_event("startup") - async def startup_event(): - """Initialize the server on startup.""" - logger.info("Starting MCP server...") - - # Sanity checks for critical configuration - if port <= 0 or port > 65535: - logger.error(f"Invalid port number: {port}") - raise ValueError(f"Invalid port number: {port}") - - if window_width <= 0 or window_height <= 0: - logger.error(f"Invalid window dimensions: {window_width}x{window_height}") - raise ValueError( - f"Invalid window dimensions: {window_width}x{window_height}" - ) + # Add a startup event + @starlette_app.on_event("startup") + async def startup_event(): + """Initialize the server on startup.""" + logger.info("Starting MCP server...") + + # Sanity checks for critical configuration + if port <= 0 or port > 65535: + logger.error(f"Invalid port number: {port}") + raise ValueError(f"Invalid port number: {port}") + + if window_width <= 0 or window_height <= 0: + logger.error(f"Invalid window dimensions: {window_width}x{window_height}") + raise ValueError( + f"Invalid window dimensions: {window_width}x{window_height}" + ) - if task_expiry_minutes <= 0: - logger.error(f"Invalid task expiry minutes: {task_expiry_minutes}") - raise ValueError(f"Invalid task expiry minutes: {task_expiry_minutes}") + if task_expiry_minutes <= 0: + logger.error(f"Invalid task expiry minutes: {task_expiry_minutes}") + raise ValueError(f"Invalid task expiry minutes: {task_expiry_minutes}") - # Start background task cleanup - asyncio.create_task(app.cleanup_old_tasks()) - logger.info("Task cleanup process scheduled") + # Start background task cleanup + asyncio.create_task(app.cleanup_old_tasks()) + logger.info("Task cleanup process scheduled") - # Run uvicorn server - uvicorn.run(starlette_app, host="0.0.0.0", port=port) + # Run uvicorn server + uvicorn.run(starlette_app, host="0.0.0.0", port=port) return 0 From 1a431ed7ae9c03482a8ef2f2bffc4fbbf43b2432 Mon Sep 17 00:00:00 2001 From: tumberger Date: Sat, 22 Mar 2025 15:30:14 -0700 Subject: [PATCH 02/12] package browser-use-mcp-server --- pyproject.toml | 3 + src/browser_use_mcp_server/__init__.py | 8 ++ src/browser_use_mcp_server/cli.py | 108 +++++++++++++++++++++++++ src/browser_use_mcp_server/server.py | 16 ++++ 4 files changed, 135 insertions(+) create mode 100644 src/browser_use_mcp_server/__init__.py create mode 100644 src/browser_use_mcp_server/cli.py create mode 100644 src/browser_use_mcp_server/server.py diff --git a/pyproject.toml b/pyproject.toml index 9679c70..7abfc66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,3 +73,6 @@ browser-use-mcp-server = "browser_use_mcp_server.cli:cli" [tool.hatch.build] packages = ["src/browser_use_mcp_server"] + +[tool.hatch.build.targets.wheel] +packages = ["src/browser_use_mcp_server"] diff --git a/src/browser_use_mcp_server/__init__.py b/src/browser_use_mcp_server/__init__.py new file mode 100644 index 0000000..853878c --- /dev/null +++ b/src/browser_use_mcp_server/__init__.py @@ -0,0 +1,8 @@ +""" +Browser-Use MCP Server Package + +This package provides a Model-Control-Protocol (MCP) server for browser automation +using the browser_use library. +""" + +__version__ = "0.1.3" diff --git a/src/browser_use_mcp_server/cli.py b/src/browser_use_mcp_server/cli.py new file mode 100644 index 0000000..bdebf34 --- /dev/null +++ b/src/browser_use_mcp_server/cli.py @@ -0,0 +1,108 @@ +""" +Command line interface for browser-use-mcp-server. + +This module provides a command-line interface for starting the browser-use MCP server. +It wraps the existing server functionality with a CLI. +""" + +import os +import sys +import click +import importlib.util + +def import_server_module(): + """ + Import the server module from the server directory. + This allows us to reuse the existing server code. + """ + # Add the root directory to the Python path to find server module + root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) + sys.path.insert(0, root_dir) + + try: + # Try to import the server module + import server.server + return server.server + except ImportError: + # If running as an installed package, the server module might be elsewhere + try: + # Look in common locations + if os.path.exists(os.path.join(root_dir, 'server', 'server.py')): + spec = importlib.util.spec_from_file_location( + "server.server", + os.path.join(root_dir, 'server', 'server.py') + ) + server_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(server_module) + return server_module + except Exception as e: + raise ImportError(f"Could not import server module: {e}") + + raise ImportError("Could not find server module. Make sure it's installed correctly.") + +@click.group() +def cli(): + """Browser-use MCP server command line interface.""" + pass + +@cli.command() +@click.argument('subcommand') +@click.option("--port", default=8000, help="Port to listen on for SSE") +@click.option("--chrome-path", default=None, help="Path to Chrome executable") +@click.option("--window-width", default=1280, help="Browser window width") +@click.option("--window-height", default=1100, help="Browser window height") +@click.option("--locale", default="en-US", help="Browser locale") +@click.option("--task-expiry-minutes", default=60, help="Minutes after which tasks are considered expired") +@click.option("--stdio", is_flag=True, default=False, help="Enable stdio mode with mcp-proxy") +def run(subcommand, port, chrome_path, window_width, window_height, locale, task_expiry_minutes, stdio): + """Run the browser-use MCP server. + + SUBCOMMAND: should be 'server' + """ + if subcommand != 'server': + click.echo(f"Unknown subcommand: {subcommand}. Only 'server' is supported.", err=True) + sys.exit(1) + + try: + # Import the server module + server_module = import_server_module() + + # We need to construct the command line arguments to pass to the server's Click command + old_argv = sys.argv.copy() + + # Build a new argument list for the server command + new_argv = [ + "server", # Program name + "--port", str(port), + ] + + if chrome_path: + new_argv.extend(["--chrome-path", chrome_path]) + + new_argv.extend(["--window-width", str(window_width)]) + new_argv.extend(["--window-height", str(window_height)]) + new_argv.extend(["--locale", locale]) + new_argv.extend(["--task-expiry-minutes", str(task_expiry_minutes)]) + + if stdio: + new_argv.append("--stdio") + + # Replace sys.argv temporarily + sys.argv = new_argv + + # Run the server's command directly + try: + return server_module.main() + finally: + # Restore original sys.argv + sys.argv = old_argv + + except Exception as e: + import traceback + click.echo(f"Error starting server: {e}", err=True) + click.echo("Detailed error:", err=True) + click.echo(traceback.format_exc(), err=True) + sys.exit(1) + +if __name__ == "__main__": + cli() \ No newline at end of file diff --git a/src/browser_use_mcp_server/server.py b/src/browser_use_mcp_server/server.py new file mode 100644 index 0000000..a976af0 --- /dev/null +++ b/src/browser_use_mcp_server/server.py @@ -0,0 +1,16 @@ +""" +Server module that re-exports the main server module. + +This provides a clean import path for the CLI and other code. +""" + +import os +import sys +import importlib.util + +# Add the root directory to the Python path to find server module +root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.insert(0, root_dir) + +# Import the server module +from server.server import * \ No newline at end of file From 0dc9ba965c64ebaa71efc0b534448fa010c4568a Mon Sep 17 00:00:00 2001 From: tumberger Date: Mon, 24 Mar 2025 13:35:21 -0700 Subject: [PATCH 03/12] Separate ports, launch mcp-proxy and sse in parallel --- server/server.py | 148 ++++++++++++++++-------------- src/browser_use_mcp_server/cli.py | 6 +- 2 files changed, 86 insertions(+), 68 deletions(-) diff --git a/server/server.py b/server/server.py index b12127c..3841034 100644 --- a/server/server.py +++ b/server/server.py @@ -11,6 +11,7 @@ # Standard library imports import os +import sys import asyncio import json import logging @@ -18,6 +19,7 @@ import uuid from datetime import datetime from typing import Any, Dict, Optional, Tuple, Union +import time # Third-party imports import click @@ -602,6 +604,7 @@ async def read_resource(uri: str) -> list[types.ResourceContents]: @click.command() @click.option("--port", default=8000, help="Port to listen on for SSE") +@click.option("--proxy-port", default=None, type=int, help="Port for the proxy to listen on. If specified, enables proxy mode.") @click.option("--chrome-path", default=None, help="Path to Chrome executable") @click.option( "--window-width", @@ -619,9 +622,10 @@ async def read_resource(uri: str) -> list[types.ResourceContents]: default=CONFIG["DEFAULT_TASK_EXPIRY_MINUTES"], help="Minutes after which tasks are considered expired", ) -@click.option("--stdio", is_flag=True, default=False, help="Enable stdio mode with mcp-proxy") +@click.option("--stdio", is_flag=True, default=False, help="Enable stdio mode. If specified, enables proxy mode.") def main( port: int, + proxy_port: Optional[int], chrome_path: str, window_width: int, window_height: int, @@ -635,14 +639,19 @@ def main( This function initializes the MCP server and runs it with the SSE transport. Each browser task will create its own isolated browser context. + The server can run in two modes: + 1. Direct SSE mode (default): Just runs the SSE server + 2. Proxy mode (enabled by --stdio or --proxy-port): Runs both SSE server and mcp-proxy + Args: port: Port to listen on for SSE + proxy_port: Port for the proxy to listen on. If specified, enables proxy mode. chrome_path: Path to Chrome executable window_width: Browser window width window_height: Browser window height locale: Browser locale task_expiry_minutes: Minutes after which tasks are considered expired - stdio: Enable stdio mode with mcp-proxy + stdio: Enable stdio mode. If specified, enables proxy mode. Returns: Exit code (0 for success) @@ -673,34 +682,86 @@ def main( from starlette.applications import Starlette from starlette.routing import Mount, Route import uvicorn + import asyncio + from concurrent.futures import ThreadPoolExecutor + import threading sse = SseServerTransport("/messages/") + # Create the Starlette app for SSE + async def handle_sse(request): + """Handle SSE connections from clients.""" + try: + async with sse.connect_sse( + request.scope, request.receive, request._send + ) as streams: + await app.run( + streams[0], streams[1], app.create_initialization_options() + ) + except Exception as e: + logger.error(f"Error in handle_sse: {str(e)}") + raise + + starlette_app = Starlette( + debug=True, + routes=[ + Route("/sse", endpoint=handle_sse), + Mount("/messages/", app=sse.handle_post_message), + ], + ) + + # Add startup event + @starlette_app.on_event("startup") + async def startup_event(): + """Initialize the server on startup.""" + logger.info("Starting MCP server...") + + # Sanity checks for critical configuration + if port <= 0 or port > 65535: + logger.error(f"Invalid port number: {port}") + raise ValueError(f"Invalid port number: {port}") + + if window_width <= 0 or window_height <= 0: + logger.error(f"Invalid window dimensions: {window_width}x{window_height}") + raise ValueError( + f"Invalid window dimensions: {window_width}x{window_height}" + ) + + if task_expiry_minutes <= 0: + logger.error(f"Invalid task expiry minutes: {task_expiry_minutes}") + raise ValueError(f"Invalid task expiry minutes: {task_expiry_minutes}") + + # Start background task cleanup + asyncio.create_task(app.cleanup_old_tasks()) + logger.info("Task cleanup process scheduled") + + # Function to run uvicorn in a separate thread + def run_uvicorn(): + uvicorn.run(starlette_app, host="0.0.0.0", port=port) + + # If proxy mode is enabled, run both the SSE server and mcp-proxy if stdio: - # When stdio is enabled, we'll use mcp-proxy to create a proxy - # that runs our server and exposes stdio import subprocess import sys - - # Get the absolute path to the current script - server_module = sys.modules[__name__] - server_file = os.path.abspath(server_module.__file__) - - # Use sys.executable to get the current Python interpreter path - python_executable = sys.executable - - # Start mcp-proxy with our server command + + # Start the SSE server in a separate thread + sse_thread = threading.Thread(target=run_uvicorn) + sse_thread.daemon = True + sse_thread.start() + + # Give the SSE server a moment to start + time.sleep(1) + proxy_cmd = [ "mcp-proxy", - "--sse-port", str(port), - "--allow-origin", "*", - "--", # Separate mcp-proxy args from command args - python_executable, server_file, "--port", str(port) + f"http://localhost:{port}/sse", + "--sse-port", str(proxy_port), + "--allow-origin", "*" ] logger.info(f"Running proxy command: {' '.join(proxy_cmd)}") + logger.info(f"SSE server running on port {port}, proxy running on port {proxy_port}") - # Run the proxy try: with subprocess.Popen(proxy_cmd) as proxy_process: proxy_process.wait() @@ -709,55 +770,8 @@ def main( logger.error(f"Command was: {' '.join(proxy_cmd)}") return 1 else: - # When stdio is disabled, we'll just run the SSE server directly - async def handle_sse(request): - """Handle SSE connections from clients.""" - try: - async with sse.connect_sse( - request.scope, request.receive, request._send - ) as streams: - await app.run( - streams[0], streams[1], app.create_initialization_options() - ) - except Exception as e: - logger.error(f"Error in handle_sse: {str(e)}") - raise - - starlette_app = Starlette( - debug=True, - routes=[ - Route("/sse", endpoint=handle_sse), - Mount("/messages/", app=sse.handle_post_message), - ], - ) - - # Add a startup event - @starlette_app.on_event("startup") - async def startup_event(): - """Initialize the server on startup.""" - logger.info("Starting MCP server...") - - # Sanity checks for critical configuration - if port <= 0 or port > 65535: - logger.error(f"Invalid port number: {port}") - raise ValueError(f"Invalid port number: {port}") - - if window_width <= 0 or window_height <= 0: - logger.error(f"Invalid window dimensions: {window_width}x{window_height}") - raise ValueError( - f"Invalid window dimensions: {window_width}x{window_height}" - ) - - if task_expiry_minutes <= 0: - logger.error(f"Invalid task expiry minutes: {task_expiry_minutes}") - raise ValueError(f"Invalid task expiry minutes: {task_expiry_minutes}") - - # Start background task cleanup - asyncio.create_task(app.cleanup_old_tasks()) - logger.info("Task cleanup process scheduled") - - # Run uvicorn server - uvicorn.run(starlette_app, host="0.0.0.0", port=port) + logger.info(f"Running in direct SSE mode on port {port}") + run_uvicorn() return 0 diff --git a/src/browser_use_mcp_server/cli.py b/src/browser_use_mcp_server/cli.py index bdebf34..11ac200 100644 --- a/src/browser_use_mcp_server/cli.py +++ b/src/browser_use_mcp_server/cli.py @@ -48,13 +48,14 @@ def cli(): @cli.command() @click.argument('subcommand') @click.option("--port", default=8000, help="Port to listen on for SSE") +@click.option("--proxy-port", default=None, type=int, help="Port for the proxy to listen on (when using stdio mode)") @click.option("--chrome-path", default=None, help="Path to Chrome executable") @click.option("--window-width", default=1280, help="Browser window width") @click.option("--window-height", default=1100, help="Browser window height") @click.option("--locale", default="en-US", help="Browser locale") @click.option("--task-expiry-minutes", default=60, help="Minutes after which tasks are considered expired") @click.option("--stdio", is_flag=True, default=False, help="Enable stdio mode with mcp-proxy") -def run(subcommand, port, chrome_path, window_width, window_height, locale, task_expiry_minutes, stdio): +def run(subcommand, port, proxy_port, chrome_path, window_width, window_height, locale, task_expiry_minutes, stdio): """Run the browser-use MCP server. SUBCOMMAND: should be 'server' @@ -79,6 +80,9 @@ def run(subcommand, port, chrome_path, window_width, window_height, locale, task if chrome_path: new_argv.extend(["--chrome-path", chrome_path]) + if proxy_port is not None: + new_argv.extend(["--proxy-port", str(proxy_port)]) + new_argv.extend(["--window-width", str(window_width)]) new_argv.extend(["--window-height", str(window_height)]) new_argv.extend(["--locale", locale]) From cffba41ed3511669e5fbf4befbd1217411e2ca08 Mon Sep 17 00:00:00 2001 From: tumberger Date: Mon, 24 Mar 2025 13:45:18 -0700 Subject: [PATCH 04/12] Update Readme --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 9b1b9ad..99adcbb 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # ➡️ browser-use mcp server -[browser-use](https://github.com/browser-use/browser-use) MCP Server with SSE +[browser-use](https://github.com/browser-use/browser-use) MCP Server with SSE + stdio transport ### requirements - uv +- mcp-proxy (for stdio) ``` curl -LsSf https://astral.sh/uv/install.sh | sh @@ -13,13 +14,23 @@ curl -LsSf https://astral.sh/uv/install.sh | sh ### quickstart -``` +Starting in SSE mode: +```bash uv sync uv pip install playwright uv run playwright install --with-deps --no-shell chromium uv run server --port 8000 ``` +With stdio mode (for terminal-based clients): +```bash +# Run with stdio mode and specify a proxy port +uv run server --stdio --proxy-port 8001 + +# Or just stdio mode (random proxy port) +uv run server --stdio +``` + - the .env requires the following: ``` @@ -27,12 +38,9 @@ OPENAI_API_KEY=[your api key] CHROME_PATH=[only change this if you have a custom chrome build] ``` -- we will be adding support for other LLM providers to power browser-use - (claude, grok, bedrock, etc) - -when building the docker image, you can use Docker secrets for VNC password: +When building the docker image, you can use Docker secrets for VNC password: -``` +```bash # With Docker secrets (recommended for production) echo "your-secure-password" > vnc_password.txt docker run -v $(pwd)/vnc_password.txt:/run/secrets/vnc_password your-image-name @@ -44,6 +52,7 @@ docker build . ### tools - [x] SSE transport +- [x] stdio transport (via mcp-proxy) - [x] browser_use - Initiates browser tasks with URL and action - [x] browser_get_result - Retrieves results of async browser tasks @@ -52,13 +61,11 @@ docker build . - cursor.ai - claude desktop - claude code -- windsurf ([windsurf](https://codeium.com/windsurf) doesn't support SSE - yet) +- windsurf ([windsurf](https://codeium.com/windsurf) doesn't support SSE, only stdio) -### usage +#### SSE Mode -after running the server, add http://localhost:8000/sse to your client UI, or in -a mcp.json file: +After running the server in SSE mode, add http://localhost:8000/sse to your client UI, or in a mcp.json file: ```json { @@ -70,6 +77,31 @@ a mcp.json file: } ``` +#### stdio Mode + +When running in stdio mode, the server will automatically start both the SSE server and mcp-proxy. The proxy handles the conversion between stdio and SSE protocols. No additional configuration is needed - just start your terminal-based client and it will communicate with the server through stdin/stdout. + +Install the cli + +```bash +uv pip install -e . +``` + +And then e.g., in Windsurf, paste: + +```json +{ + "mcpServers": { + "browser-server": { + "command": "browser-use-mcp-server", + "args": ["run", "server", "--port", "8000", "--stdio", "--proxy-port", "9000"] + } + } +} +``` + +### client configuration paths + #### cursor - `./.cursor/mcp.json` @@ -83,7 +115,9 @@ a mcp.json file: - `~/Library/Application Support/Claude/claude_desktop_config.json` - `%APPDATA%\Claude\claude_desktop_config.json` -then try asking your LLM the following: +### example usage + +Try asking your LLM the following: `open https://news.ycombinator.com and return the top ranked article` From d057d6a7ee6e5fa3c53f01ffd273d1428b1442a5 Mon Sep 17 00:00:00 2001 From: tumberger Date: Mon, 24 Mar 2025 13:50:20 -0700 Subject: [PATCH 05/12] Update Readme --- README.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 99adcbb..fcf5cfa 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ # ➡️ browser-use mcp server +[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/cobrowser.svg?style=social&label=Follow%20%40cobrowser)](https://x.com/cobrowser) + [browser-use](https://github.com/browser-use/browser-use) MCP Server with SSE + stdio transport -### requirements + + +### Requirements - uv - mcp-proxy (for stdio) @@ -12,7 +16,7 @@ transport curl -LsSf https://astral.sh/uv/install.sh | sh ``` -### quickstart +### Quickstart Starting in SSE mode: ```bash @@ -49,14 +53,14 @@ docker run -v $(pwd)/vnc_password.txt:/run/secrets/vnc_password your-image-name docker build . ``` -### tools +### Tools - [x] SSE transport - [x] stdio transport (via mcp-proxy) - [x] browser_use - Initiates browser tasks with URL and action - [x] browser_get_result - Retrieves results of async browser tasks -### supported clients +### Supported Clients - cursor.ai - claude desktop @@ -100,32 +104,32 @@ And then e.g., in Windsurf, paste: } ``` -### client configuration paths +### Client Configuration Paths -#### cursor +#### Cursor - `./.cursor/mcp.json` -#### windsurf +#### Windsurf - `~/.codeium/windsurf/mcp_config.json` -#### claude +#### Claude - `~/Library/Application Support/Claude/claude_desktop_config.json` - `%APPDATA%\Claude\claude_desktop_config.json` -### example usage +### Example Usage Try asking your LLM the following: `open https://news.ycombinator.com and return the top ranked article` -### help +### Help for issues or interest reach out @ https://cobrowser.xyz -# stars +# Stars From a245a540680fa2044960ca42a82e801c0a8fde29 Mon Sep 17 00:00:00 2001 From: tumberger Date: Mon, 24 Mar 2025 13:51:40 -0700 Subject: [PATCH 06/12] Add pypi package version --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index fcf5cfa..e49574b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # ➡️ browser-use mcp server [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/cobrowser.svg?style=social&label=Follow%20%40cobrowser)](https://x.com/cobrowser) +[![PyPI version](https://badge.fury.io/py/browser-use-mcp-server.svg)](https://pypi.org/project/browser-use-mcp-server/) + [browser-use](https://github.com/browser-use/browser-use) MCP Server with SSE + stdio transport From e6a41aa55c5607d17c26d646659def6d492da098 Mon Sep 17 00:00:00 2001 From: tumberger Date: Mon, 24 Mar 2025 13:55:44 -0700 Subject: [PATCH 07/12] Udpate Readme --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e49574b..e8bc82d 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,14 @@ transport ### Requirements -- uv -- mcp-proxy (for stdio) +- [uv](https://github.com/astral-sh/uv) +- [mcp-proxy](https://github.com/sparfenyuk/mcp-proxy) (for stdio) ``` +# 1. Install uv curl -LsSf https://astral.sh/uv/install.sh | sh +# 2. Install mcp-proxy pypi package via uv +uv tool install mcp-proxy ``` ### Quickstart From 3bfbd70b7bebdec770612319acfea7ec0e63f831 Mon Sep 17 00:00:00 2001 From: tumberger Date: Mon, 24 Mar 2025 18:50:18 -0700 Subject: [PATCH 08/12] lint --- server/server.py | 33 +++++++---- src/browser_use_mcp_server/cli.py | 85 +++++++++++++++++++--------- src/browser_use_mcp_server/server.py | 28 +++++++-- 3 files changed, 104 insertions(+), 42 deletions(-) diff --git a/server/server.py b/server/server.py index 3841034..8bc5541 100644 --- a/server/server.py +++ b/server/server.py @@ -11,7 +11,6 @@ # Standard library imports import os -import sys import asyncio import json import logging @@ -604,7 +603,12 @@ async def read_resource(uri: str) -> list[types.ResourceContents]: @click.command() @click.option("--port", default=8000, help="Port to listen on for SSE") -@click.option("--proxy-port", default=None, type=int, help="Port for the proxy to listen on. If specified, enables proxy mode.") +@click.option( + "--proxy-port", + default=None, + type=int, + help="Port for the proxy to listen on. If specified, enables proxy mode.", +) @click.option("--chrome-path", default=None, help="Path to Chrome executable") @click.option( "--window-width", @@ -622,7 +626,12 @@ async def read_resource(uri: str) -> list[types.ResourceContents]: default=CONFIG["DEFAULT_TASK_EXPIRY_MINUTES"], help="Minutes after which tasks are considered expired", ) -@click.option("--stdio", is_flag=True, default=False, help="Enable stdio mode. If specified, enables proxy mode.") +@click.option( + "--stdio", + is_flag=True, + default=False, + help="Enable stdio mode. If specified, enables proxy mode.", +) def main( port: int, proxy_port: Optional[int], @@ -683,7 +692,6 @@ def main( from starlette.routing import Mount, Route import uvicorn import asyncio - from concurrent.futures import ThreadPoolExecutor import threading sse = SseServerTransport("/messages/") @@ -742,11 +750,10 @@ def run_uvicorn(): # If proxy mode is enabled, run both the SSE server and mcp-proxy if stdio: import subprocess - import sys # Start the SSE server in a separate thread sse_thread = threading.Thread(target=run_uvicorn) - sse_thread.daemon = True + sse_thread.daemon = True sse_thread.start() # Give the SSE server a moment to start @@ -754,14 +761,18 @@ def run_uvicorn(): proxy_cmd = [ "mcp-proxy", - f"http://localhost:{port}/sse", - "--sse-port", str(proxy_port), - "--allow-origin", "*" + f"http://localhost:{port}/sse", + "--sse-port", + str(proxy_port), + "--allow-origin", + "*", ] logger.info(f"Running proxy command: {' '.join(proxy_cmd)}") - logger.info(f"SSE server running on port {port}, proxy running on port {proxy_port}") - + logger.info( + f"SSE server running on port {port}, proxy running on port {proxy_port}" + ) + try: with subprocess.Popen(proxy_cmd) as proxy_process: proxy_process.wait() diff --git a/src/browser_use_mcp_server/cli.py b/src/browser_use_mcp_server/cli.py index 11ac200..5642b2b 100644 --- a/src/browser_use_mcp_server/cli.py +++ b/src/browser_use_mcp_server/cli.py @@ -10,103 +10,134 @@ import click import importlib.util + def import_server_module(): """ Import the server module from the server directory. This allows us to reuse the existing server code. """ # Add the root directory to the Python path to find server module - root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) + root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) sys.path.insert(0, root_dir) - + try: # Try to import the server module import server.server + return server.server except ImportError: # If running as an installed package, the server module might be elsewhere try: # Look in common locations - if os.path.exists(os.path.join(root_dir, 'server', 'server.py')): + if os.path.exists(os.path.join(root_dir, "server", "server.py")): spec = importlib.util.spec_from_file_location( - "server.server", - os.path.join(root_dir, 'server', 'server.py') + "server.server", os.path.join(root_dir, "server", "server.py") ) server_module = importlib.util.module_from_spec(spec) spec.loader.exec_module(server_module) return server_module except Exception as e: raise ImportError(f"Could not import server module: {e}") - - raise ImportError("Could not find server module. Make sure it's installed correctly.") + + raise ImportError( + "Could not find server module. Make sure it's installed correctly." + ) + @click.group() def cli(): """Browser-use MCP server command line interface.""" pass + @cli.command() -@click.argument('subcommand') +@click.argument("subcommand") @click.option("--port", default=8000, help="Port to listen on for SSE") -@click.option("--proxy-port", default=None, type=int, help="Port for the proxy to listen on (when using stdio mode)") +@click.option( + "--proxy-port", + default=None, + type=int, + help="Port for the proxy to listen on (when using stdio mode)", +) @click.option("--chrome-path", default=None, help="Path to Chrome executable") @click.option("--window-width", default=1280, help="Browser window width") @click.option("--window-height", default=1100, help="Browser window height") @click.option("--locale", default="en-US", help="Browser locale") -@click.option("--task-expiry-minutes", default=60, help="Minutes after which tasks are considered expired") -@click.option("--stdio", is_flag=True, default=False, help="Enable stdio mode with mcp-proxy") -def run(subcommand, port, proxy_port, chrome_path, window_width, window_height, locale, task_expiry_minutes, stdio): +@click.option( + "--task-expiry-minutes", + default=60, + help="Minutes after which tasks are considered expired", +) +@click.option( + "--stdio", is_flag=True, default=False, help="Enable stdio mode with mcp-proxy" +) +def run( + subcommand, + port, + proxy_port, + chrome_path, + window_width, + window_height, + locale, + task_expiry_minutes, + stdio, +): """Run the browser-use MCP server. - + SUBCOMMAND: should be 'server' """ - if subcommand != 'server': - click.echo(f"Unknown subcommand: {subcommand}. Only 'server' is supported.", err=True) + if subcommand != "server": + click.echo( + f"Unknown subcommand: {subcommand}. Only 'server' is supported.", err=True + ) sys.exit(1) - + try: # Import the server module server_module = import_server_module() - + # We need to construct the command line arguments to pass to the server's Click command old_argv = sys.argv.copy() - + # Build a new argument list for the server command new_argv = [ "server", # Program name - "--port", str(port), + "--port", + str(port), ] - + if chrome_path: new_argv.extend(["--chrome-path", chrome_path]) - + if proxy_port is not None: new_argv.extend(["--proxy-port", str(proxy_port)]) - + new_argv.extend(["--window-width", str(window_width)]) new_argv.extend(["--window-height", str(window_height)]) new_argv.extend(["--locale", locale]) new_argv.extend(["--task-expiry-minutes", str(task_expiry_minutes)]) - + if stdio: new_argv.append("--stdio") - + # Replace sys.argv temporarily sys.argv = new_argv - + # Run the server's command directly try: return server_module.main() finally: # Restore original sys.argv sys.argv = old_argv - + except Exception as e: import traceback + click.echo(f"Error starting server: {e}", err=True) click.echo("Detailed error:", err=True) click.echo(traceback.format_exc(), err=True) sys.exit(1) + if __name__ == "__main__": - cli() \ No newline at end of file + cli() diff --git a/src/browser_use_mcp_server/server.py b/src/browser_use_mcp_server/server.py index a976af0..f0b463c 100644 --- a/src/browser_use_mcp_server/server.py +++ b/src/browser_use_mcp_server/server.py @@ -6,11 +6,31 @@ import os import sys -import importlib.util +from server.server import ( + Server, + main, + create_browser_context_for_task, + run_browser_task_async, + cleanup_old_tasks, + create_mcp_server, + init_configuration, + CONFIG, + task_store, +) # Add the root directory to the Python path to find server module -root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) sys.path.insert(0, root_dir) -# Import the server module -from server.server import * \ No newline at end of file +# Re-export everything we imported +__all__ = [ + 'Server', + 'main', + 'create_browser_context_for_task', + 'run_browser_task_async', + 'cleanup_old_tasks', + 'create_mcp_server', + 'init_configuration', + 'CONFIG', + 'task_store', +] From 37942964d43a9cb7802a9038455266b828613e35 Mon Sep 17 00:00:00 2001 From: tumberger Date: Mon, 24 Mar 2025 23:22:46 -0700 Subject: [PATCH 09/12] update --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e8bc82d..e378435 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,9 @@ [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/cobrowser.svg?style=social&label=Follow%20%40cobrowser)](https://x.com/cobrowser) [![PyPI version](https://badge.fury.io/py/browser-use-mcp-server.svg)](https://pypi.org/project/browser-use-mcp-server/) - [browser-use](https://github.com/browser-use/browser-use) MCP Server with SSE + stdio transport - - ### Requirements - [uv](https://github.com/astral-sh/uv) @@ -24,6 +21,7 @@ uv tool install mcp-proxy ### Quickstart Starting in SSE mode: + ```bash uv sync uv pip install playwright @@ -32,6 +30,7 @@ uv run server --port 8000 ``` With stdio mode (for terminal-based clients): + ```bash # Run with stdio mode and specify a proxy port uv run server --stdio --proxy-port 8001 @@ -93,7 +92,7 @@ When running in stdio mode, the server will automatically start both the SSE ser Install the cli ```bash -uv pip install -e . +uv pip install -e . ``` And then e.g., in Windsurf, paste: @@ -102,8 +101,16 @@ And then e.g., in Windsurf, paste: { "mcpServers": { "browser-server": { - "command": "browser-use-mcp-server", - "args": ["run", "server", "--port", "8000", "--stdio", "--proxy-port", "9000"] + "command": "browser-use-mcp-server", + "args": [ + "run", + "server", + "--port", + "8000", + "--stdio", + "--proxy-port", + "9000" + ] } } } From 7dfb602c2078a82152b27216b021afdcf5c1ec9b Mon Sep 17 00:00:00 2001 From: Michel Osswald Date: Mon, 24 Mar 2025 23:28:18 -0700 Subject: [PATCH 10/12] linting --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e378435..9714e6f 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/cobrowser.svg?style=social&label=Follow%20%40cobrowser)](https://x.com/cobrowser) [![PyPI version](https://badge.fury.io/py/browser-use-mcp-server.svg)](https://pypi.org/project/browser-use-mcp-server/) -[browser-use](https://github.com/browser-use/browser-use) MCP Server with SSE + stdio -transport +[browser-use](https://github.com/browser-use/browser-use) MCP Server with SSE + +stdio transport ### Requirements @@ -69,11 +69,13 @@ docker build . - cursor.ai - claude desktop - claude code -- windsurf ([windsurf](https://codeium.com/windsurf) doesn't support SSE, only stdio) +- windsurf ([windsurf](https://codeium.com/windsurf) doesn't support SSE, only + stdio) #### SSE Mode -After running the server in SSE mode, add http://localhost:8000/sse to your client UI, or in a mcp.json file: +After running the server in SSE mode, add http://localhost:8000/sse to your +client UI, or in a mcp.json file: ```json { @@ -87,7 +89,11 @@ After running the server in SSE mode, add http://localhost:8000/sse to your clie #### stdio Mode -When running in stdio mode, the server will automatically start both the SSE server and mcp-proxy. The proxy handles the conversion between stdio and SSE protocols. No additional configuration is needed - just start your terminal-based client and it will communicate with the server through stdin/stdout. +When running in stdio mode, the server will automatically start both the SSE +server and mcp-proxy. The proxy handles the conversion between stdio and SSE +protocols. No additional configuration is needed - just start your +terminal-based client and it will communicate with the server through +stdin/stdout. Install the cli From a13cb05aeb8bbeadce7e86ece28cf764dab21de5 Mon Sep 17 00:00:00 2001 From: Michel Osswald Date: Mon, 24 Mar 2025 23:30:36 -0700 Subject: [PATCH 11/12] python linting --- src/browser_use_mcp_server/server.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/browser_use_mcp_server/server.py b/src/browser_use_mcp_server/server.py index f0b463c..a101b80 100644 --- a/src/browser_use_mcp_server/server.py +++ b/src/browser_use_mcp_server/server.py @@ -24,13 +24,13 @@ # Re-export everything we imported __all__ = [ - 'Server', - 'main', - 'create_browser_context_for_task', - 'run_browser_task_async', - 'cleanup_old_tasks', - 'create_mcp_server', - 'init_configuration', - 'CONFIG', - 'task_store', + "Server", + "main", + "create_browser_context_for_task", + "run_browser_task_async", + "cleanup_old_tasks", + "create_mcp_server", + "init_configuration", + "CONFIG", + "task_store", ] From ed7cd033c7fcfae84d364cc42a0ff49f552c8572 Mon Sep 17 00:00:00 2001 From: Michel Osswald Date: Tue, 25 Mar 2025 08:11:02 -0700 Subject: [PATCH 12/12] reademe update --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9714e6f..4337444 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ uv run playwright install --with-deps --no-shell chromium uv run server --port 8000 ``` -With stdio mode (for terminal-based clients): +With stdio mode: ```bash # Run with stdio mode and specify a proxy port @@ -91,9 +91,8 @@ client UI, or in a mcp.json file: When running in stdio mode, the server will automatically start both the SSE server and mcp-proxy. The proxy handles the conversion between stdio and SSE -protocols. No additional configuration is needed - just start your -terminal-based client and it will communicate with the server through -stdin/stdout. +protocols. No additional configuration is needed - just start your client and it +will communicate with the server through stdin/stdout. Install the cli