Skip to content

Commit 3c0a089

Browse files
committed
add mcp tool
1 parent 9093106 commit 3c0a089

File tree

4 files changed

+138
-17
lines changed

4 files changed

+138
-17
lines changed

requirements.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
browser-use==0.1.40
1+
browser-use==0.1.41
22
pyperclip==1.9.0
3-
gradio==5.23.1
3+
gradio==5.27.0
44
json-repair
55
langchain-mistralai==0.2.4
6-
langchain-google-genai==2.0.8
76
MainContentExtractor==0.0.4
8-
langchain-ibm==0.3.10
7+
langchain-ibm==0.3.10
8+
langchain_mcp_adapters==0.0.9
Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pdb
22

33
import pyperclip
4-
from typing import Optional, Type
4+
from typing import Optional, Type, Callable, Dict, Any, Union, Awaitable
55
from pydantic import BaseModel
66
from browser_use.agent.views import ActionResult
77
from browser_use.browser.context import BrowserContext
@@ -20,30 +20,78 @@
2020
SwitchTabAction,
2121
)
2222
import logging
23+
import inspect
24+
import os
25+
from src.utils import utils
2326

2427
logger = logging.getLogger(__name__)
2528

2629

2730
class CustomController(Controller):
2831
def __init__(self, exclude_actions: list[str] = [],
29-
output_model: Optional[Type[BaseModel]] = None
32+
output_model: Optional[Type[BaseModel]] = None,
33+
ask_assistant_callback: Optional[Union[Callable[[str, BrowserContext], Dict[str, Any]], Callable[
34+
[str, BrowserContext], Awaitable[Dict[str, Any]]]]] = None,
35+
3036
):
3137
super().__init__(exclude_actions=exclude_actions, output_model=output_model)
3238
self._register_custom_actions()
39+
self.ask_assistant_callback = ask_assistant_callback
3340

3441
def _register_custom_actions(self):
3542
"""Register all custom browser actions"""
3643

37-
@self.registry.action("Copy text to clipboard")
38-
def copy_to_clipboard(text: str):
39-
pyperclip.copy(text)
40-
return ActionResult(extracted_content=text)
44+
@self.registry.action(
45+
"When executing tasks, prioritize autonomous completion. However, if you encounter a definitive blocker "
46+
"that prevents you from proceeding independently – such as needing credentials you don't possess, "
47+
"requiring subjective human judgment, needing a physical action performed, encountering complex CAPTCHAs, "
48+
"or facing limitations in your capabilities – you must request human assistance."
49+
)
50+
async def ask_for_assistant(query: str, browser: BrowserContext):
51+
if self.ask_assistant_callback:
52+
if inspect.iscoroutinefunction(self.ask_assistant_callback):
53+
user_response = await self.ask_assistant_callback(query, browser)
54+
else:
55+
user_response = self.ask_assistant_callback(query, browser)
56+
msg = f"AI ask: {query}. User response: {user_response['response']}"
57+
logger.info(msg)
58+
return ActionResult(extracted_content=msg, include_in_memory=True)
59+
else:
60+
return ActionResult(extracted_content="Human cannot help you. Please try another way.",
61+
include_in_memory=True)
62+
63+
@self.registry.action(
64+
'Upload file to interactive element with file path ',
65+
)
66+
async def upload_file(index: int, path: str, browser: BrowserContext, available_file_paths: list[str]):
67+
if path not in available_file_paths:
68+
return ActionResult(error=f'File path {path} is not available')
69+
70+
if not os.path.exists(path):
71+
return ActionResult(error=f'File {path} does not exist')
72+
73+
dom_el = await browser.get_dom_element_by_index(index)
74+
75+
file_upload_dom_el = dom_el.get_file_upload_element()
76+
77+
if file_upload_dom_el is None:
78+
msg = f'No file upload element found at index {index}'
79+
logger.info(msg)
80+
return ActionResult(error=msg)
81+
82+
file_upload_el = await browser.get_locate_element(file_upload_dom_el)
4183

42-
@self.registry.action("Paste text from clipboard")
43-
async def paste_from_clipboard(browser: BrowserContext):
44-
text = pyperclip.paste()
45-
# send text to browser
46-
page = await browser.get_current_page()
47-
await page.keyboard.type(text)
84+
if file_upload_el is None:
85+
msg = f'No file upload element found at index {index}'
86+
logger.info(msg)
87+
return ActionResult(error=msg)
4888

49-
return ActionResult(extracted_content=text)
89+
try:
90+
await file_upload_el.set_input_files(path)
91+
msg = f'Successfully uploaded file to index {index}'
92+
logger.info(msg)
93+
return ActionResult(extracted_content=msg, include_in_memory=True)
94+
except Exception as e:
95+
msg = f'Failed to upload file to index {index}: {str(e)}'
96+
logger.info(msg)
97+
return ActionResult(error=msg)

src/utils/mcp_client.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import os
2+
import asyncio
3+
import base64
4+
import pdb
5+
from typing import List, Tuple, Optional
6+
from langchain_core.tools import BaseTool
7+
from langchain_mcp_adapters.client import MultiServerMCPClient
8+
import base64
9+
import json
10+
import logging
11+
from typing import Optional, Dict, Any, Type
12+
from langchain_core.tools import BaseTool
13+
from pydantic.v1 import BaseModel, Field
14+
from langchain_core.runnables import RunnableConfig
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
async def setup_mcp_client_and_tools(mcp_server_config: Dict[str, Any]) -> Tuple[
20+
Optional[List[BaseTool]], Optional[MultiServerMCPClient]]:
21+
"""
22+
Initializes the MultiServerMCPClient, connects to servers, fetches tools,
23+
filters them, and returns a flat list of usable tools and the client instance.
24+
25+
Returns:
26+
A tuple containing:
27+
- list[BaseTool]: The filtered list of usable LangChain tools.
28+
- MultiServerMCPClient | None: The initialized and started client instance, or None on failure.
29+
"""
30+
31+
logger.info("Initializing MultiServerMCPClient...")
32+
33+
try:
34+
client = MultiServerMCPClient(mcp_server_config)
35+
await client.__aenter__()
36+
mcp_tools = client.get_tools()
37+
logger.info(f"Total usable MCP tools collected: {len(mcp_tools)}")
38+
return mcp_tools, client
39+
40+
except Exception as e:
41+
logger.error(f"Failed to setup MCP client or fetch tools: {e}", exc_info=True)
42+
return [], None

tests/test_controller.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import asyncio
2+
import pdb
3+
import sys
4+
5+
sys.path.append(".")
6+
7+
from dotenv import load_dotenv
8+
9+
load_dotenv()
10+
11+
12+
async def test_mcp_client():
13+
from src.utils.mcp_client import setup_mcp_client_and_tools
14+
15+
test_server_config = {
16+
"playwright": {
17+
"command": "npx",
18+
"args": [
19+
"@playwright/mcp@latest",
20+
],
21+
"transport": "stdio",
22+
}
23+
}
24+
25+
mcp_tools, mcp_client = await setup_mcp_client_and_tools(test_server_config)
26+
27+
pdb.set_trace()
28+
29+
30+
if __name__ == '__main__':
31+
asyncio.run(test_mcp_client())

0 commit comments

Comments
 (0)