browser-mcp-server 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- browser_mcp_server-0.2.0/LICENSE +21 -0
- browser_mcp_server-0.2.0/PKG-INFO +185 -0
- browser_mcp_server-0.2.0/README.md +159 -0
- browser_mcp_server-0.2.0/pyproject.toml +40 -0
- browser_mcp_server-0.2.0/src/mcp_browser_use/__init__.py +6 -0
- browser_mcp_server-0.2.0/src/mcp_browser_use/server.py +363 -0
- browser_mcp_server-0.2.0/src/mcp_browser_use/tools.py +130 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Your Name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: browser-mcp-server
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: MCP server that gives any LLM agent full browser control via browser-use. Auto-state after every action.
|
|
5
|
+
Project-URL: Homepage, https://github.com/your-username/browser-mcp-server
|
|
6
|
+
Project-URL: Repository, https://github.com/your-username/browser-mcp-server
|
|
7
|
+
Project-URL: Issues, https://github.com/your-username/browser-mcp-server/issues
|
|
8
|
+
Author-email: Your Name <you@example.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,automation,browser,browser-use,llm,mcp,playwright
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: browser-use>=0.12.0
|
|
24
|
+
Requires-Dist: mcp>=1.0.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# browser-mcp-server
|
|
28
|
+
|
|
29
|
+
> Full browser control for any LLM agent via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io).
|
|
30
|
+
|
|
31
|
+
Powered by [browser-use](https://github.com/browser-use/browser-use) — a persistent, Playwright-backed browser session that your agent controls step-by-step.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## ✨ Key Feature: Zero State Calls
|
|
36
|
+
|
|
37
|
+
Every action that changes the page (`open`, `click`, `input`, `type`, `keys`, `scroll`, `back`) **automatically returns the updated page state** in the same response. Your agent never wastes a round-trip calling `state` after an action — it's already there.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install browser-mcp-server
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> **Chromium is installed automatically on first run.** The server detects whether
|
|
48
|
+
> the browser binary is present and downloads it if not (~170 MB, one-time).
|
|
49
|
+
> You never need to run `playwright install chromium` manually.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
### Claude Desktop
|
|
56
|
+
|
|
57
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"browser-use": {
|
|
63
|
+
"command": "browser-mcp-server"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Cursor / Zed / Windsurf
|
|
70
|
+
|
|
71
|
+
Add to your MCP settings:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcpServers": {
|
|
76
|
+
"browser-use": {
|
|
77
|
+
"command": "browser-mcp-server"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### nixagent (`mcp.json`)
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"mcpServers": {
|
|
88
|
+
"browser-use": {
|
|
89
|
+
"command": "browser-mcp-server",
|
|
90
|
+
"active": true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from nixagent import Agent
|
|
98
|
+
|
|
99
|
+
agent = Agent(
|
|
100
|
+
name="BrowserAgent",
|
|
101
|
+
system_prompt="You are a web browsing assistant.",
|
|
102
|
+
mcp_config_path="mcp.json"
|
|
103
|
+
)
|
|
104
|
+
agent.run("Go to stripe.com and tell me the pricing for the Starter plan.")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### HTTP / Remote Agents
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Modern streaming (recommended)
|
|
111
|
+
browser-mcp-server --transport streamable-http --port 8080
|
|
112
|
+
|
|
113
|
+
# Legacy SSE
|
|
114
|
+
browser-mcp-server --transport sse --port 8080
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Available Tools
|
|
120
|
+
|
|
121
|
+
| Tool | Description |
|
|
122
|
+
| -------------------------- | --------------------------------------------------------- |
|
|
123
|
+
| `browser_use_tool` | Generic dispatcher — any browser-use command |
|
|
124
|
+
| `browser_open_tool` | Open URL →**state auto-returned** |
|
|
125
|
+
| `browser_click_tool` | Click by index →**state auto-returned** |
|
|
126
|
+
| `browser_input_tool` | Click + type (preferred) →**state auto-returned** |
|
|
127
|
+
| `browser_type_tool` | Type into focused element →**state auto-returned** |
|
|
128
|
+
| `browser_keys_tool` | Send keyboard key →**state auto-returned** |
|
|
129
|
+
| `browser_scroll_tool` | Scroll up/down →**state auto-returned** |
|
|
130
|
+
| `browser_back_tool` | Navigate back →**state auto-returned** |
|
|
131
|
+
| `browser_state_tool` | Explicit state fetch*(rarely needed)* |
|
|
132
|
+
| `browser_get_text_tool` | Extract element text |
|
|
133
|
+
| `browser_get_html_tool` | Full/scoped page HTML |
|
|
134
|
+
| `browser_get_title_tool` | Page title |
|
|
135
|
+
| `browser_close_tool` | Close all sessions*(call when done)* |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## How the Auto-State Works
|
|
140
|
+
|
|
141
|
+
Traditional browser agents need two calls to interact:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
1. agent calls: state → get element indexes
|
|
145
|
+
2. agent calls: click 3 → click
|
|
146
|
+
3. agent calls: state → get updated indexes ← wasted call
|
|
147
|
+
4. agent calls: input 7 "..." → type
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
With `browser-mcp-server`, every mutating action returns fresh state in its own response:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
1. agent calls: open https://... → navigated + state returned ✓
|
|
154
|
+
2. agent calls: click 3 → clicked + updated state returned ✓
|
|
155
|
+
3. agent calls: input 7 "..." → typed + updated state returned ✓
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## CLI Reference
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
usage: browser-mcp-server [-h] [--transport {stdio,sse,streamable-http}]
|
|
164
|
+
[--host HOST] [--port PORT]
|
|
165
|
+
|
|
166
|
+
options:
|
|
167
|
+
--transport stdio | sse | streamable-http (default: stdio)
|
|
168
|
+
--host host for HTTP transports (default: 127.0.0.1)
|
|
169
|
+
--port port for HTTP transports (default: 8080)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Requirements
|
|
175
|
+
|
|
176
|
+
- Python 3.10+
|
|
177
|
+
- `browser-use >= 0.12.0`
|
|
178
|
+
- `mcp >= 1.0.0`
|
|
179
|
+
- Chromium (installed via `playwright install chromium`)
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# browser-mcp-server
|
|
2
|
+
|
|
3
|
+
> Full browser control for any LLM agent via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io).
|
|
4
|
+
|
|
5
|
+
Powered by [browser-use](https://github.com/browser-use/browser-use) — a persistent, Playwright-backed browser session that your agent controls step-by-step.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Key Feature: Zero State Calls
|
|
10
|
+
|
|
11
|
+
Every action that changes the page (`open`, `click`, `input`, `type`, `keys`, `scroll`, `back`) **automatically returns the updated page state** in the same response. Your agent never wastes a round-trip calling `state` after an action — it's already there.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install browser-mcp-server
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> **Chromium is installed automatically on first run.** The server detects whether
|
|
22
|
+
> the browser binary is present and downloads it if not (~170 MB, one-time).
|
|
23
|
+
> You never need to run `playwright install chromium` manually.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### Claude Desktop
|
|
30
|
+
|
|
31
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"browser-use": {
|
|
37
|
+
"command": "browser-mcp-server"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Cursor / Zed / Windsurf
|
|
44
|
+
|
|
45
|
+
Add to your MCP settings:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"browser-use": {
|
|
51
|
+
"command": "browser-mcp-server"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### nixagent (`mcp.json`)
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"browser-use": {
|
|
63
|
+
"command": "browser-mcp-server",
|
|
64
|
+
"active": true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from nixagent import Agent
|
|
72
|
+
|
|
73
|
+
agent = Agent(
|
|
74
|
+
name="BrowserAgent",
|
|
75
|
+
system_prompt="You are a web browsing assistant.",
|
|
76
|
+
mcp_config_path="mcp.json"
|
|
77
|
+
)
|
|
78
|
+
agent.run("Go to stripe.com and tell me the pricing for the Starter plan.")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### HTTP / Remote Agents
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Modern streaming (recommended)
|
|
85
|
+
browser-mcp-server --transport streamable-http --port 8080
|
|
86
|
+
|
|
87
|
+
# Legacy SSE
|
|
88
|
+
browser-mcp-server --transport sse --port 8080
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Available Tools
|
|
94
|
+
|
|
95
|
+
| Tool | Description |
|
|
96
|
+
| -------------------------- | --------------------------------------------------------- |
|
|
97
|
+
| `browser_use_tool` | Generic dispatcher — any browser-use command |
|
|
98
|
+
| `browser_open_tool` | Open URL →**state auto-returned** |
|
|
99
|
+
| `browser_click_tool` | Click by index →**state auto-returned** |
|
|
100
|
+
| `browser_input_tool` | Click + type (preferred) →**state auto-returned** |
|
|
101
|
+
| `browser_type_tool` | Type into focused element →**state auto-returned** |
|
|
102
|
+
| `browser_keys_tool` | Send keyboard key →**state auto-returned** |
|
|
103
|
+
| `browser_scroll_tool` | Scroll up/down →**state auto-returned** |
|
|
104
|
+
| `browser_back_tool` | Navigate back →**state auto-returned** |
|
|
105
|
+
| `browser_state_tool` | Explicit state fetch*(rarely needed)* |
|
|
106
|
+
| `browser_get_text_tool` | Extract element text |
|
|
107
|
+
| `browser_get_html_tool` | Full/scoped page HTML |
|
|
108
|
+
| `browser_get_title_tool` | Page title |
|
|
109
|
+
| `browser_close_tool` | Close all sessions*(call when done)* |
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## How the Auto-State Works
|
|
114
|
+
|
|
115
|
+
Traditional browser agents need two calls to interact:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
1. agent calls: state → get element indexes
|
|
119
|
+
2. agent calls: click 3 → click
|
|
120
|
+
3. agent calls: state → get updated indexes ← wasted call
|
|
121
|
+
4. agent calls: input 7 "..." → type
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
With `browser-mcp-server`, every mutating action returns fresh state in its own response:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
1. agent calls: open https://... → navigated + state returned ✓
|
|
128
|
+
2. agent calls: click 3 → clicked + updated state returned ✓
|
|
129
|
+
3. agent calls: input 7 "..." → typed + updated state returned ✓
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## CLI Reference
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
usage: browser-mcp-server [-h] [--transport {stdio,sse,streamable-http}]
|
|
138
|
+
[--host HOST] [--port PORT]
|
|
139
|
+
|
|
140
|
+
options:
|
|
141
|
+
--transport stdio | sse | streamable-http (default: stdio)
|
|
142
|
+
--host host for HTTP transports (default: 127.0.0.1)
|
|
143
|
+
--port port for HTTP transports (default: 8080)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Requirements
|
|
149
|
+
|
|
150
|
+
- Python 3.10+
|
|
151
|
+
- `browser-use >= 0.12.0`
|
|
152
|
+
- `mcp >= 1.0.0`
|
|
153
|
+
- Chromium (installed via `playwright install chromium`)
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "browser-mcp-server"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "MCP server that gives any LLM agent full browser control via browser-use. Auto-state after every action."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Your Name", email = "you@example.com" }]
|
|
12
|
+
keywords = ["mcp", "browser", "browser-use", "llm", "agent", "automation", "playwright"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
"Topic :: Internet :: WWW/HTTP :: Browsers",
|
|
24
|
+
]
|
|
25
|
+
requires-python = ">=3.10"
|
|
26
|
+
dependencies = [
|
|
27
|
+
"mcp>=1.0.0",
|
|
28
|
+
"browser-use>=0.12.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/your-username/browser-mcp-server"
|
|
33
|
+
Repository = "https://github.com/your-username/browser-mcp-server"
|
|
34
|
+
Issues = "https://github.com/your-username/browser-mcp-server/issues"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
browser-mcp-server = "mcp_browser_use.server:main"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/mcp_browser_use"]
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mcp_browser_use/server.py
|
|
3
|
+
──────────────────────────
|
|
4
|
+
FastMCP server exposing the browser-use CLI as MCP tools.
|
|
5
|
+
|
|
6
|
+
Transport options
|
|
7
|
+
-----------------
|
|
8
|
+
stdio — Claude Desktop, nixagent, Cursor, Zed, etc. (default)
|
|
9
|
+
streamable-http — Remote / networked agents (recommended for HTTP)
|
|
10
|
+
sse — Legacy HTTP server-sent events
|
|
11
|
+
|
|
12
|
+
Usage
|
|
13
|
+
-----
|
|
14
|
+
# stdio (most common)
|
|
15
|
+
mcp-browser-use
|
|
16
|
+
|
|
17
|
+
# HTTP
|
|
18
|
+
mcp-browser-use --transport streamable-http --port 8080
|
|
19
|
+
|
|
20
|
+
Chromium auto-install
|
|
21
|
+
---------------------
|
|
22
|
+
On first run the server checks whether the Chromium binary is present.
|
|
23
|
+
If it is missing it runs `playwright install chromium` automatically.
|
|
24
|
+
Subsequent starts skip the download entirely (binary already on disk).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import os
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
from mcp.server.fastmcp import FastMCP
|
|
33
|
+
|
|
34
|
+
from .tools import (
|
|
35
|
+
browser_use,
|
|
36
|
+
browser_open,
|
|
37
|
+
browser_state,
|
|
38
|
+
browser_click,
|
|
39
|
+
browser_input,
|
|
40
|
+
browser_type,
|
|
41
|
+
browser_keys,
|
|
42
|
+
browser_scroll,
|
|
43
|
+
browser_back,
|
|
44
|
+
browser_get_text,
|
|
45
|
+
browser_get_html,
|
|
46
|
+
browser_get_title,
|
|
47
|
+
browser_close,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# ── Chromium auto-install ─────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
def _ensure_chromium() -> None:
|
|
53
|
+
"""
|
|
54
|
+
Check if the Playwright Chromium binary is present.
|
|
55
|
+
If it is missing, download it automatically via `playwright install chromium`.
|
|
56
|
+
|
|
57
|
+
This runs at most once — subsequent calls return immediately because
|
|
58
|
+
the binary is already on disk.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
from playwright.sync_api import sync_playwright # noqa: PLC0415
|
|
62
|
+
|
|
63
|
+
with sync_playwright() as p:
|
|
64
|
+
executable = p.chromium.executable_path
|
|
65
|
+
if os.path.exists(executable):
|
|
66
|
+
return # Already installed — nothing to do
|
|
67
|
+
except Exception:
|
|
68
|
+
pass # playwright not importable or path check failed → fall through
|
|
69
|
+
|
|
70
|
+
# Binary missing — download it now
|
|
71
|
+
print(
|
|
72
|
+
"[mcp-browser-use] Chromium not found. Installing automatically "
|
|
73
|
+
"(one-time download, ~170 MB)...",
|
|
74
|
+
file=sys.stderr,
|
|
75
|
+
)
|
|
76
|
+
subprocess.run(
|
|
77
|
+
[sys.executable, "-m", "playwright", "install", "chromium"],
|
|
78
|
+
check=True,
|
|
79
|
+
)
|
|
80
|
+
print("[mcp-browser-use] Chromium installed successfully.", file=sys.stderr)
|
|
81
|
+
|
|
82
|
+
def create_server() -> FastMCP:
|
|
83
|
+
"""
|
|
84
|
+
Build and return the configured FastMCP server instance.
|
|
85
|
+
Exposed so the server can be embedded or tested programmatically.
|
|
86
|
+
"""
|
|
87
|
+
mcp = FastMCP(
|
|
88
|
+
name="browser-use",
|
|
89
|
+
instructions=(
|
|
90
|
+
"Controls a persistent browser session via the browser-use CLI. "
|
|
91
|
+
"After any mutating command (open, click, input, type, keys, "
|
|
92
|
+
"scroll, back) the updated page state is returned automatically — "
|
|
93
|
+
"never call browser_state after one of these. "
|
|
94
|
+
"Always finish a task with browser_close."
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# ── tool: browser_use (generic dispatcher) ────────────────────────────────
|
|
99
|
+
@mcp.tool(
|
|
100
|
+
description=(
|
|
101
|
+
"Execute any browser-use CLI command.\n\n"
|
|
102
|
+
"Mutating commands automatically return the updated page state — "
|
|
103
|
+
"no need to call browser_state after them.\n\n"
|
|
104
|
+
"Commands:\n"
|
|
105
|
+
" 'open https://example.com' → navigate, state auto-returned\n"
|
|
106
|
+
" 'click 3' → click element, state auto-returned\n"
|
|
107
|
+
" 'input 5 \"query\"' → click+type, state auto-returned\n"
|
|
108
|
+
" 'type \"hello\"' → type into focused element\n"
|
|
109
|
+
" 'keys \"Enter\"' → send keyboard key\n"
|
|
110
|
+
" 'scroll down' / 'scroll up' → scroll page\n"
|
|
111
|
+
" 'back' → navigate back\n"
|
|
112
|
+
" 'get text 2' → extract element text\n"
|
|
113
|
+
" 'get html' → full page HTML\n"
|
|
114
|
+
" 'get html --selector \"h1\"' → scoped HTML\n"
|
|
115
|
+
" 'get title' → page title\n"
|
|
116
|
+
" 'close --all' → close all sessions"
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
def browser_use_tool(command: str, headed: bool = False) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Run a browser-use CLI command.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
command: Sub-command string (e.g. 'open https://example.com').
|
|
125
|
+
headed: Show the browser window. Only applies to 'open'. Default false.
|
|
126
|
+
"""
|
|
127
|
+
return browser_use(command, headed=headed)
|
|
128
|
+
|
|
129
|
+
# ── tool: browser_open ────────────────────────────────────────────────────
|
|
130
|
+
@mcp.tool(
|
|
131
|
+
description=(
|
|
132
|
+
"Open a URL in the browser. "
|
|
133
|
+
"The current page state (URL, title, clickable elements) "
|
|
134
|
+
"is returned automatically."
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
def browser_open_tool(url: str, headed: bool = False) -> str:
|
|
138
|
+
"""
|
|
139
|
+
Navigate to a URL and return the page state.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
url: Full URL to open (e.g. 'https://news.ycombinator.com').
|
|
143
|
+
headed: Show the browser window. Default false.
|
|
144
|
+
"""
|
|
145
|
+
return browser_open(url, headed=headed)
|
|
146
|
+
|
|
147
|
+
# ── tool: browser_state ───────────────────────────────────────────────────
|
|
148
|
+
@mcp.tool(
|
|
149
|
+
description=(
|
|
150
|
+
"Get the current browser page state: URL, title, and a numbered list "
|
|
151
|
+
"of all interactive elements with their indexes. "
|
|
152
|
+
"Rarely needed — mutating commands already return state automatically."
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
def browser_state_tool() -> str:
|
|
156
|
+
"""Explicitly fetch the current page state."""
|
|
157
|
+
return browser_state()
|
|
158
|
+
|
|
159
|
+
# ── tool: browser_click ───────────────────────────────────────────────────
|
|
160
|
+
@mcp.tool(
|
|
161
|
+
description=(
|
|
162
|
+
"Click an element by its index (from the page state list). "
|
|
163
|
+
"The updated page state is returned automatically after clicking."
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
def browser_click_tool(index: int) -> str:
|
|
167
|
+
"""
|
|
168
|
+
Click the element at the given index.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
index: Element index from the page state list.
|
|
172
|
+
"""
|
|
173
|
+
return browser_click(index)
|
|
174
|
+
|
|
175
|
+
# ── tool: browser_input ───────────────────────────────────────────────────
|
|
176
|
+
@mcp.tool(
|
|
177
|
+
description=(
|
|
178
|
+
"Click an element and type text into it in one step. "
|
|
179
|
+
"Preferred over separate click + type. "
|
|
180
|
+
"The updated page state is returned automatically."
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
def browser_input_tool(index: int, text: str) -> str:
|
|
184
|
+
"""
|
|
185
|
+
Click element at index, then type text.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
index: Element index from the page state list.
|
|
189
|
+
text: Text to type.
|
|
190
|
+
"""
|
|
191
|
+
return browser_input(index, text)
|
|
192
|
+
|
|
193
|
+
# ── tool: browser_type ────────────────────────────────────────────────────
|
|
194
|
+
@mcp.tool(
|
|
195
|
+
description=(
|
|
196
|
+
"Type text into the currently focused element. "
|
|
197
|
+
"The updated page state is returned automatically."
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
def browser_type_tool(text: str) -> str:
|
|
201
|
+
"""
|
|
202
|
+
Type into the focused element.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
text: The text to type.
|
|
206
|
+
"""
|
|
207
|
+
return browser_type(text)
|
|
208
|
+
|
|
209
|
+
# ── tool: browser_keys ────────────────────────────────────────────────────
|
|
210
|
+
@mcp.tool(
|
|
211
|
+
description=(
|
|
212
|
+
"Send a keyboard key to the browser "
|
|
213
|
+
"(e.g. 'Enter', 'Tab', 'Escape', 'ArrowDown'). "
|
|
214
|
+
"The updated page state is returned automatically."
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
def browser_keys_tool(key: str) -> str:
|
|
218
|
+
"""
|
|
219
|
+
Press a keyboard key.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
key: Key name (e.g. 'Enter', 'Tab', 'Escape', 'ArrowDown').
|
|
223
|
+
"""
|
|
224
|
+
return browser_keys(key)
|
|
225
|
+
|
|
226
|
+
# ── tool: browser_scroll ──────────────────────────────────────────────────
|
|
227
|
+
@mcp.tool(
|
|
228
|
+
description=(
|
|
229
|
+
"Scroll the current page up or down. "
|
|
230
|
+
"The updated page state is returned automatically."
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
def browser_scroll_tool(direction: str = "down") -> str:
|
|
234
|
+
"""
|
|
235
|
+
Scroll the page.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
direction: 'down' or 'up'. Defaults to 'down'.
|
|
239
|
+
"""
|
|
240
|
+
if direction not in ("up", "down"):
|
|
241
|
+
return "Error: direction must be 'up' or 'down'."
|
|
242
|
+
return browser_scroll(direction)
|
|
243
|
+
|
|
244
|
+
# ── tool: browser_back ────────────────────────────────────────────────────
|
|
245
|
+
@mcp.tool(
|
|
246
|
+
description=(
|
|
247
|
+
"Navigate back in browser history. "
|
|
248
|
+
"The updated page state is returned automatically."
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
def browser_back_tool() -> str:
|
|
252
|
+
"""Go back one page."""
|
|
253
|
+
return browser_back()
|
|
254
|
+
|
|
255
|
+
# ── tool: browser_get_text ────────────────────────────────────────────────
|
|
256
|
+
@mcp.tool(
|
|
257
|
+
description="Get the text content of a specific element by its index number."
|
|
258
|
+
)
|
|
259
|
+
def browser_get_text_tool(index: int) -> str:
|
|
260
|
+
"""
|
|
261
|
+
Extract the visible text of an element.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
index: Element index from the page state list.
|
|
265
|
+
"""
|
|
266
|
+
return browser_get_text(index)
|
|
267
|
+
|
|
268
|
+
# ── tool: browser_get_html ────────────────────────────────────────────────
|
|
269
|
+
@mcp.tool(
|
|
270
|
+
description=(
|
|
271
|
+
"Get the HTML of the current page, or of a specific element "
|
|
272
|
+
"using a CSS selector. Useful for scraping structured data."
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
def browser_get_html_tool(selector: str = "") -> str:
|
|
276
|
+
"""
|
|
277
|
+
Retrieve page HTML.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
selector: Optional CSS selector (e.g. 'h1', '.price').
|
|
281
|
+
Leave empty for full page HTML.
|
|
282
|
+
"""
|
|
283
|
+
return browser_get_html(selector if selector else None)
|
|
284
|
+
|
|
285
|
+
# ── tool: browser_get_title ───────────────────────────────────────────────
|
|
286
|
+
@mcp.tool(description="Get the title of the current browser page.")
|
|
287
|
+
def browser_get_title_tool() -> str:
|
|
288
|
+
"""Return the current page title."""
|
|
289
|
+
return browser_get_title()
|
|
290
|
+
|
|
291
|
+
# ── tool: browser_close ───────────────────────────────────────────────────
|
|
292
|
+
@mcp.tool(
|
|
293
|
+
description=(
|
|
294
|
+
"Close all browser sessions. "
|
|
295
|
+
"ALWAYS call this when the task is fully complete."
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
def browser_close_tool() -> str:
|
|
299
|
+
"""Close all browser sessions and end the task."""
|
|
300
|
+
return browser_close()
|
|
301
|
+
|
|
302
|
+
return mcp
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ── CLI entry point ───────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
def main() -> None:
|
|
308
|
+
parser = argparse.ArgumentParser(
|
|
309
|
+
prog="browser-mcp-server",
|
|
310
|
+
description="browser-use MCP server — expose browser automation to any LLM agent",
|
|
311
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
312
|
+
epilog="""
|
|
313
|
+
Transport modes:
|
|
314
|
+
stdio Local agents: Claude Desktop, Cursor, nixagent, Zed. (default)
|
|
315
|
+
streamable-http Modern HTTP streaming — recommended for remote agents.
|
|
316
|
+
sse Legacy HTTP server-sent events.
|
|
317
|
+
|
|
318
|
+
Examples:
|
|
319
|
+
browser-mcp-server
|
|
320
|
+
browser-mcp-server --transport streamable-http --port 8080
|
|
321
|
+
browser-mcp-server --transport sse --host 0.0.0.0 --port 9000
|
|
322
|
+
""",
|
|
323
|
+
)
|
|
324
|
+
parser.add_argument(
|
|
325
|
+
"--transport",
|
|
326
|
+
choices=["stdio", "sse", "streamable-http"],
|
|
327
|
+
default="stdio",
|
|
328
|
+
help="MCP transport (default: stdio)",
|
|
329
|
+
)
|
|
330
|
+
parser.add_argument(
|
|
331
|
+
"--host",
|
|
332
|
+
default="127.0.0.1",
|
|
333
|
+
help="Host for HTTP transports (default: 127.0.0.1)",
|
|
334
|
+
)
|
|
335
|
+
parser.add_argument(
|
|
336
|
+
"--port",
|
|
337
|
+
type=int,
|
|
338
|
+
default=8080,
|
|
339
|
+
help="Port for HTTP transports (default: 8080)",
|
|
340
|
+
)
|
|
341
|
+
args = parser.parse_args()
|
|
342
|
+
|
|
343
|
+
# Ensure Chromium is present before the server starts accepting requests.
|
|
344
|
+
# No-op if already installed; downloads automatically on first run.
|
|
345
|
+
_ensure_chromium()
|
|
346
|
+
|
|
347
|
+
server = create_server()
|
|
348
|
+
|
|
349
|
+
if args.transport == "stdio":
|
|
350
|
+
server.run(transport="stdio")
|
|
351
|
+
else:
|
|
352
|
+
server.host = args.host
|
|
353
|
+
server.port = args.port
|
|
354
|
+
print(
|
|
355
|
+
f"[mcp-browser-use] Starting '{args.transport}' server on "
|
|
356
|
+
f"http://{args.host}:{args.port}",
|
|
357
|
+
file=sys.stderr,
|
|
358
|
+
)
|
|
359
|
+
server.run(transport=args.transport)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
if __name__ == "__main__":
|
|
363
|
+
main()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mcp_browser_use/tools.py
|
|
3
|
+
─────────────────────────
|
|
4
|
+
Thin wrapper around the `browser-use` CLI.
|
|
5
|
+
|
|
6
|
+
Auto-state behaviour
|
|
7
|
+
────────────────────
|
|
8
|
+
Every command that mutates the page (open, click, input, type, keys, scroll,
|
|
9
|
+
back) automatically fetches and appends `browser-use state` to its output.
|
|
10
|
+
The LLM therefore NEVER needs to call `state` explicitly — it always has the
|
|
11
|
+
current element list in the response of the previous action.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import subprocess
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Commands that mutate the page → state is auto-appended after each of these.
|
|
19
|
+
_MUTATING_CMDS = {"open", "click", "input", "type", "keys", "scroll", "back"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ── Low-level runner ──────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
def _run(args: list[str], timeout: int = 30) -> str:
|
|
25
|
+
"""Run `browser-use <args>` and return stdout+stderr. Raises on failure."""
|
|
26
|
+
cmd = ["browser-use"] + args
|
|
27
|
+
try:
|
|
28
|
+
result = subprocess.run(
|
|
29
|
+
cmd, capture_output=True, text=True, timeout=timeout
|
|
30
|
+
)
|
|
31
|
+
output = (result.stdout + result.stderr).strip()
|
|
32
|
+
if result.returncode != 0:
|
|
33
|
+
raise RuntimeError(
|
|
34
|
+
f"browser-use failed (exit {result.returncode}):\n{output}"
|
|
35
|
+
)
|
|
36
|
+
return output
|
|
37
|
+
except FileNotFoundError:
|
|
38
|
+
raise RuntimeError(
|
|
39
|
+
"browser-use CLI not found. "
|
|
40
|
+
"Install it with: pip install browser-use && playwright install"
|
|
41
|
+
)
|
|
42
|
+
except subprocess.TimeoutExpired:
|
|
43
|
+
raise RuntimeError(
|
|
44
|
+
f"browser-use timed out after {timeout}s: {' '.join(cmd)}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── Primary dispatcher ────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
def browser_use(command: str, headed: bool = False) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Run any browser-use CLI command.
|
|
53
|
+
|
|
54
|
+
Mutating commands (open, click, input, type, keys, scroll, back)
|
|
55
|
+
automatically append the updated page state to their output so the LLM
|
|
56
|
+
never needs an explicit `state` call after an action.
|
|
57
|
+
"""
|
|
58
|
+
parts = command.strip().split(maxsplit=1)
|
|
59
|
+
sub_cmd = parts[0].lower() if parts else ""
|
|
60
|
+
|
|
61
|
+
args: list[str] = []
|
|
62
|
+
if headed and sub_cmd == "open":
|
|
63
|
+
args.append("--headed")
|
|
64
|
+
args += command.strip().split()
|
|
65
|
+
|
|
66
|
+
output = _run(args)
|
|
67
|
+
|
|
68
|
+
if sub_cmd in _MUTATING_CMDS:
|
|
69
|
+
label = f"browser-use: after '{sub_cmd}' — current state (auto-fetched)"
|
|
70
|
+
try:
|
|
71
|
+
state_output = _run(["state"])
|
|
72
|
+
output = f"{output}\n\n[{label}]\n{state_output}"
|
|
73
|
+
except RuntimeError as exc:
|
|
74
|
+
output += f"\n\n[browser-use: state fetch failed — {exc}]"
|
|
75
|
+
|
|
76
|
+
return output
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── Convenience wrappers ──────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
def browser_open(url: str, headed: bool = False) -> str:
|
|
82
|
+
"""Open a URL. Page state is returned automatically."""
|
|
83
|
+
return browser_use(f"open {url}", headed=headed)
|
|
84
|
+
|
|
85
|
+
def browser_state() -> str:
|
|
86
|
+
"""Get the current page state (URL, title, element list)."""
|
|
87
|
+
return _run(["state"])
|
|
88
|
+
|
|
89
|
+
def browser_click(index: int) -> str:
|
|
90
|
+
"""Click the element at index. Page state auto-returned."""
|
|
91
|
+
return browser_use(f"click {index}")
|
|
92
|
+
|
|
93
|
+
def browser_input(index: int, text: str) -> str:
|
|
94
|
+
"""Click element at index then type text. Page state auto-returned."""
|
|
95
|
+
return browser_use(f'input {index} "{text}"')
|
|
96
|
+
|
|
97
|
+
def browser_type(text: str) -> str:
|
|
98
|
+
"""Type into the currently focused element. Page state auto-returned."""
|
|
99
|
+
return browser_use(f'type "{text}"')
|
|
100
|
+
|
|
101
|
+
def browser_keys(key: str) -> str:
|
|
102
|
+
"""Send a keyboard key (e.g. 'Enter'). Page state auto-returned."""
|
|
103
|
+
return browser_use(f'keys "{key}"')
|
|
104
|
+
|
|
105
|
+
def browser_scroll(direction: str = "down") -> str:
|
|
106
|
+
"""Scroll up or down. Page state auto-returned."""
|
|
107
|
+
assert direction in ("up", "down"), "direction must be 'up' or 'down'"
|
|
108
|
+
return browser_use(f"scroll {direction}")
|
|
109
|
+
|
|
110
|
+
def browser_back() -> str:
|
|
111
|
+
"""Navigate back. Page state auto-returned."""
|
|
112
|
+
return browser_use("back")
|
|
113
|
+
|
|
114
|
+
def browser_get_text(index: int) -> str:
|
|
115
|
+
"""Get the text content of the element at index."""
|
|
116
|
+
return browser_use(f"get text {index}")
|
|
117
|
+
|
|
118
|
+
def browser_get_html(selector: Optional[str] = None) -> str:
|
|
119
|
+
"""Get full page HTML, or HTML of a specific CSS selector."""
|
|
120
|
+
if selector:
|
|
121
|
+
return browser_use(f'get html --selector "{selector}"')
|
|
122
|
+
return browser_use("get html")
|
|
123
|
+
|
|
124
|
+
def browser_get_title() -> str:
|
|
125
|
+
"""Get the current page title."""
|
|
126
|
+
return browser_use("get title")
|
|
127
|
+
|
|
128
|
+
def browser_close() -> str:
|
|
129
|
+
"""Close all browser sessions. Call when the task is done."""
|
|
130
|
+
return browser_use("close --all")
|