feat: init MCP server
This commit is contained in:
parent
576090439b
commit
e08a6b313a
152
openmanus_server/README.md
Normal file
152
openmanus_server/README.md
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
# OpenManus-server 🤖
|
||||||
|
|
||||||
|
This project provides a server based on [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) that exposes **OpenManus** tool functionalities as standardized APIs.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
This MCP server provides access to the following OpenManus tools:
|
||||||
|
|
||||||
|
1. **Browser Automation** 🌐 - Navigate webpages, click elements, input text, and more
|
||||||
|
2. **Google Search** 🔍 - Execute searches and retrieve result links
|
||||||
|
3. **Python Code Execution** 🐍 - Run Python code in a secure environment
|
||||||
|
4. **File Saving** 💾 - Save content to local files
|
||||||
|
5. **Termination Control** 🛑 - Control program execution flow
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Python 3.10+
|
||||||
|
- OpenManus project dependencies
|
||||||
|
|
||||||
|
### Installation Steps
|
||||||
|
|
||||||
|
1. First, install the OpenManus project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/mannaandpoem/OpenManus.git
|
||||||
|
cd OpenManus
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using uv (recommended)
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
uv venv
|
||||||
|
source .venv/bin/activate # Unix/macOS
|
||||||
|
# or .venv\Scripts\activate # Windows
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install MCP dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install mcp-python
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 Usage
|
||||||
|
|
||||||
|
### Starting the MCP Server
|
||||||
|
|
||||||
|
The server supports two communication modes: stdio and HTTP.
|
||||||
|
|
||||||
|
#### stdio mode (default)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python mcp_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### HTTP mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python mcp_server.py --transport http --host 127.0.0.1 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Line Arguments
|
||||||
|
|
||||||
|
- `--transport`: Communication method, choose "stdio" or "http" (default: stdio)
|
||||||
|
- `--host`: HTTP server host address (default: 127.0.0.1)
|
||||||
|
- `--port`: HTTP server port (default: 8000)
|
||||||
|
|
||||||
|
## 💻 Client Example
|
||||||
|
|
||||||
|
Check out `mcp_client_example.py` to learn how to connect to the server and call tools using the MCP client.
|
||||||
|
|
||||||
|
### Running the Client Example
|
||||||
|
|
||||||
|
1. First, start the server in HTTP mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python mcp_server.py --transport http
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In another terminal, run the client example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python mcp_client_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤖 LLM Integration
|
||||||
|
|
||||||
|
The MCP server can be integrated with LLMs that support tool calling, such as Claude 3 Opus/Sonnet/Haiku.
|
||||||
|
|
||||||
|
### Example with Claude
|
||||||
|
|
||||||
|
```python
|
||||||
|
import anthropic
|
||||||
|
from mcp.client import MCPClient
|
||||||
|
|
||||||
|
# Initialize Claude client
|
||||||
|
client = anthropic.Anthropic(api_key="your_api_key")
|
||||||
|
|
||||||
|
# Connect to MCP server
|
||||||
|
mcp_client = await MCPClient.create_http("http://localhost:8000")
|
||||||
|
|
||||||
|
# Get tool definitions
|
||||||
|
tools = await mcp_client.list_tools()
|
||||||
|
tool_definitions = [tool.to_dict() for tool in tools]
|
||||||
|
|
||||||
|
# Create Claude message
|
||||||
|
message = client.messages.create(
|
||||||
|
model="claude-3-opus-20240229",
|
||||||
|
max_tokens=1000,
|
||||||
|
temperature=0,
|
||||||
|
system="You are a helpful assistant that can use tools to help users.",
|
||||||
|
messages=[{"role": "user", "content": "Search for Model Context Protocol and summarize the top 3 results"}],
|
||||||
|
tools=tool_definitions
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle tool calls
|
||||||
|
for tool_call in message.content:
|
||||||
|
if hasattr(tool_call, "tool_use"):
|
||||||
|
tool_name = tool_call.tool_use.name
|
||||||
|
tool_params = tool_call.tool_use.input
|
||||||
|
|
||||||
|
# Call MCP tool
|
||||||
|
result = await mcp_client.invoke_tool(tool_name, tool_params)
|
||||||
|
|
||||||
|
# Send results back to Claude
|
||||||
|
message = client.messages.create(
|
||||||
|
model="claude-3-opus-20240229",
|
||||||
|
max_tokens=1000,
|
||||||
|
temperature=0,
|
||||||
|
system="You are a helpful assistant that can use tools to help users.",
|
||||||
|
messages=[
|
||||||
|
{"role": "user", "content": "Search for Model Context Protocol and summarize the top 3 results"},
|
||||||
|
{"role": "assistant", "content": [tool_call]},
|
||||||
|
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": tool_call.tool_use.id, "content": result}]}
|
||||||
|
],
|
||||||
|
tools=tool_definitions
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 Security Considerations
|
||||||
|
|
||||||
|
- By default, the HTTP server only listens on localhost (127.0.0.1) and is not exposed externally
|
||||||
|
- When using in production, ensure proper authentication and authorization mechanisms are in place
|
||||||
|
- The Python execution tool has timeout limits to prevent long-running code
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
Same license as the OpenManus project
|
3
openmanus_server/mcp_requirements.txt
Normal file
3
openmanus_server/mcp_requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Core dependencies
|
||||||
|
mcp
|
||||||
|
httpx>=0.27.0
|
174
openmanus_server/openmanus_client_example.py
Normal file
174
openmanus_server/openmanus_client_example.py
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import asyncio
|
||||||
|
from typing import Optional
|
||||||
|
from contextlib import AsyncExitStack
|
||||||
|
import os
|
||||||
|
|
||||||
|
from mcp import ClientSession, StdioServerParameters
|
||||||
|
from mcp.client.stdio import stdio_client
|
||||||
|
|
||||||
|
class OpenManusClient:
|
||||||
|
def __init__(self):
|
||||||
|
# Initialize session and client objects
|
||||||
|
self.session: Optional[ClientSession] = None
|
||||||
|
self.exit_stack = AsyncExitStack()
|
||||||
|
self.stdio = None
|
||||||
|
self.write = None
|
||||||
|
|
||||||
|
async def connect_to_server(self, server_script_path: str):
|
||||||
|
"""Connect to an MCP server via stdio
|
||||||
|
|
||||||
|
Args:
|
||||||
|
server_script_path: Path to the server script
|
||||||
|
"""
|
||||||
|
if not server_script_path.endswith('.py'):
|
||||||
|
raise ValueError("Server script must be a .py file")
|
||||||
|
|
||||||
|
# Get the current directory to add to PYTHONPATH
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
project_root = os.path.dirname(current_dir) # Get parent directory
|
||||||
|
|
||||||
|
# Prepare environment variables
|
||||||
|
env = os.environ.copy() # Copy current environment
|
||||||
|
|
||||||
|
# Add current directory and project root to PYTHONPATH
|
||||||
|
path_separator = ';' if os.name == 'nt' else ':' # Use ; for Windows, : for Unix
|
||||||
|
if 'PYTHONPATH' in env:
|
||||||
|
env['PYTHONPATH'] = f"{current_dir}{path_separator}{project_root}{path_separator}{env['PYTHONPATH']}"
|
||||||
|
else:
|
||||||
|
env['PYTHONPATH'] = f"{current_dir}{path_separator}{project_root}"
|
||||||
|
|
||||||
|
server_params = StdioServerParameters(
|
||||||
|
command="python",
|
||||||
|
args=[server_script_path],
|
||||||
|
env=env
|
||||||
|
)
|
||||||
|
|
||||||
|
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
|
||||||
|
self.stdio, self.write = stdio_transport
|
||||||
|
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
|
||||||
|
await self.session.initialize()
|
||||||
|
|
||||||
|
# List available tools
|
||||||
|
response = await self.session.list_tools()
|
||||||
|
tools = response.tools
|
||||||
|
print("\nConnected to server with tools:", [tool.name for tool in tools])
|
||||||
|
return tools
|
||||||
|
|
||||||
|
async def run_examples(self):
|
||||||
|
"""Run example tool calls to demonstrate functionality"""
|
||||||
|
try:
|
||||||
|
print("\nExample 1: Google Search")
|
||||||
|
search_result = await self.session.call_tool(
|
||||||
|
"google_search",
|
||||||
|
{"query": "Model Context Protocol", "num_results": 5}
|
||||||
|
)
|
||||||
|
print(f"Search results: {search_result.content}")
|
||||||
|
|
||||||
|
print("\nExample 2: Python Code Execution")
|
||||||
|
code = """
|
||||||
|
import math
|
||||||
|
result = 0
|
||||||
|
for i in range(1, 10):
|
||||||
|
result += math.sqrt(i)
|
||||||
|
print(f"Calculation result: {result}")
|
||||||
|
"""
|
||||||
|
python_result = await self.session.call_tool(
|
||||||
|
"python_execute",
|
||||||
|
{"code": code, "timeout": 3}
|
||||||
|
)
|
||||||
|
print(f"Python execution result: {python_result.content}")
|
||||||
|
|
||||||
|
print("\nExample 3: File Saving")
|
||||||
|
file_result = await self.session.call_tool(
|
||||||
|
"file_saver",
|
||||||
|
{
|
||||||
|
"content": "This is a test file content saved through MCP",
|
||||||
|
"file_path": "mcp_test_file.txt"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
print(f"File save result: {file_result.content}")
|
||||||
|
|
||||||
|
print("\nExample 4: Browser Usage")
|
||||||
|
# Navigate to webpage
|
||||||
|
browser_result = await self.session.call_tool(
|
||||||
|
"browser_use",
|
||||||
|
{"action": "navigate", "url": "https://www.example.com"}
|
||||||
|
)
|
||||||
|
print(f"Browser navigation result: {browser_result.content}")
|
||||||
|
|
||||||
|
# Get browser state
|
||||||
|
state_result = await self.session.call_tool(
|
||||||
|
"get_browser_state",
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
print(f"Browser state: {state_result.content}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError during example execution: {str(e)}")
|
||||||
|
|
||||||
|
async def chat_loop(self):
|
||||||
|
"""Run an interactive chat loop for testing tools"""
|
||||||
|
print("\nOpenManus MCP Client Started!")
|
||||||
|
print("Type your commands or 'quit' to exit.")
|
||||||
|
print("Available commands: google_search, python_execute, file_saver, browser_use, get_browser_state")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
command = input("\nCommand: ").strip()
|
||||||
|
|
||||||
|
if command.lower() == 'quit':
|
||||||
|
break
|
||||||
|
|
||||||
|
# Parse command and parameters
|
||||||
|
parts = command.split(maxsplit=1)
|
||||||
|
if len(parts) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
tool_name = parts[0]
|
||||||
|
tool_args = {}
|
||||||
|
if len(parts) > 1:
|
||||||
|
try:
|
||||||
|
tool_args = eval(parts[1]) # Convert string to dict
|
||||||
|
except:
|
||||||
|
print("Invalid arguments format. Please provide a valid Python dictionary.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
result = await self.session.call_tool(tool_name, tool_args)
|
||||||
|
print("\nResult:", result.content)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError: {str(e)}")
|
||||||
|
|
||||||
|
async def cleanup(self):
|
||||||
|
"""Clean up resources"""
|
||||||
|
if self.session:
|
||||||
|
await self.session.close()
|
||||||
|
await self.exit_stack.aclose()
|
||||||
|
print("\nClosed MCP client connection")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main entry point"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python openmanus_client_example.py <path_to_server_script>")
|
||||||
|
print("Example: python openmanus_client_example.py ../mcp_server.py")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
client = OpenManusClient()
|
||||||
|
try:
|
||||||
|
await client.connect_to_server(server_script_path=sys.argv[1])
|
||||||
|
|
||||||
|
# Run examples first
|
||||||
|
await client.run_examples()
|
||||||
|
|
||||||
|
# Then start interactive chat loop
|
||||||
|
await client.chat_loop()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {str(e)}")
|
||||||
|
finally:
|
||||||
|
await client.cleanup()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
177
openmanus_server/openmanus_server.py
Normal file
177
openmanus_server/openmanus_server.py
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
from typing import Optional
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("mcp-server")
|
||||||
|
|
||||||
|
# Import OpenManus tools
|
||||||
|
from app.tool.browser_use_tool import BrowserUseTool
|
||||||
|
from app.tool.google_search import GoogleSearch
|
||||||
|
from app.tool.python_execute import PythonExecute
|
||||||
|
from app.tool.file_saver import FileSaver
|
||||||
|
from app.tool.terminate import Terminate
|
||||||
|
|
||||||
|
# Initialize FastMCP server
|
||||||
|
openmanus = FastMCP("openmanus")
|
||||||
|
|
||||||
|
# Initialize tool instances
|
||||||
|
browser_tool = BrowserUseTool()
|
||||||
|
google_search_tool = GoogleSearch()
|
||||||
|
python_execute_tool = PythonExecute()
|
||||||
|
file_saver_tool = FileSaver()
|
||||||
|
terminate_tool = Terminate()
|
||||||
|
|
||||||
|
# Browser tool
|
||||||
|
@openmanus.tool()
|
||||||
|
async def browser_use(
|
||||||
|
action: str,
|
||||||
|
url: Optional[str] = None,
|
||||||
|
index: Optional[int] = None,
|
||||||
|
text: Optional[str] = None,
|
||||||
|
script: Optional[str] = None,
|
||||||
|
scroll_amount: Optional[int] = None,
|
||||||
|
tab_id: Optional[int] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Execute various browser operations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action: The browser operation to execute, possible values include:
|
||||||
|
- navigate: Navigate to specified URL
|
||||||
|
- click: Click on an element on the page
|
||||||
|
- input_text: Input text into a text field
|
||||||
|
- screenshot: Take a screenshot of the current page
|
||||||
|
- get_html: Get HTML of the current page
|
||||||
|
- get_text: Get text content of the current page
|
||||||
|
- execute_js: Execute JavaScript code
|
||||||
|
- scroll: Scroll the page
|
||||||
|
- switch_tab: Switch to specified tab
|
||||||
|
- new_tab: Open new tab
|
||||||
|
- close_tab: Close current tab
|
||||||
|
- refresh: Refresh current page
|
||||||
|
url: URL for 'navigate' or 'new_tab' operations
|
||||||
|
index: Element index for 'click' or 'input_text' operations
|
||||||
|
text: Text for 'input_text' operation
|
||||||
|
script: JavaScript code for 'execute_js' operation
|
||||||
|
scroll_amount: Scroll pixels for 'scroll' operation (positive for down, negative for up)
|
||||||
|
tab_id: Tab ID for 'switch_tab' operation
|
||||||
|
"""
|
||||||
|
logger.info(f"Executing browser operation: {action}")
|
||||||
|
result = await browser_tool.execute(
|
||||||
|
action=action,
|
||||||
|
url=url,
|
||||||
|
index=index,
|
||||||
|
text=text,
|
||||||
|
script=script,
|
||||||
|
scroll_amount=scroll_amount,
|
||||||
|
tab_id=tab_id
|
||||||
|
)
|
||||||
|
return json.dumps(result.model_dump())
|
||||||
|
|
||||||
|
@openmanus.tool()
|
||||||
|
async def get_browser_state() -> str:
|
||||||
|
"""Get current browser state, including URL, title, tabs and interactive elements."""
|
||||||
|
logger.info("Getting browser state")
|
||||||
|
result = await browser_tool.get_current_state()
|
||||||
|
return json.dumps(result.model_dump())
|
||||||
|
|
||||||
|
# Google search tool
|
||||||
|
@openmanus.tool()
|
||||||
|
async def google_search(query: str, num_results: int = 10) -> str:
|
||||||
|
"""Execute Google search and return list of relevant links.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search query
|
||||||
|
num_results: Number of results to return (default is 10)
|
||||||
|
"""
|
||||||
|
logger.info(f"Executing Google search: {query}")
|
||||||
|
results = await google_search_tool.execute(query=query, num_results=num_results)
|
||||||
|
return json.dumps(results)
|
||||||
|
|
||||||
|
# Python execution tool
|
||||||
|
@openmanus.tool()
|
||||||
|
async def python_execute(code: str, timeout: int = 5) -> str:
|
||||||
|
"""Execute Python code and return results.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: Python code to execute
|
||||||
|
timeout: Execution timeout in seconds
|
||||||
|
"""
|
||||||
|
logger.info("Executing Python code")
|
||||||
|
result = await python_execute_tool.execute(code=code, timeout=timeout)
|
||||||
|
return json.dumps(result)
|
||||||
|
|
||||||
|
# File saver tool
|
||||||
|
@openmanus.tool()
|
||||||
|
async def file_saver(content: str, file_path: str, mode: str = "w") -> str:
|
||||||
|
"""Save content to local file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Content to save
|
||||||
|
file_path: File path
|
||||||
|
mode: File open mode (default is 'w')
|
||||||
|
"""
|
||||||
|
logger.info(f"Saving file: {file_path}")
|
||||||
|
result = await file_saver_tool.execute(content=content, file_path=file_path, mode=mode)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Terminate tool
|
||||||
|
@openmanus.tool()
|
||||||
|
async def terminate(status: str) -> str:
|
||||||
|
"""Terminate program execution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
status: Termination status, can be 'success' or 'failure'
|
||||||
|
"""
|
||||||
|
logger.info(f"Terminating execution: {status}")
|
||||||
|
result = await terminate_tool.execute(status=status)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Clean up resources
|
||||||
|
async def cleanup():
|
||||||
|
"""Clean up all tool resources"""
|
||||||
|
logger.info("Cleaning up resources")
|
||||||
|
await browser_tool.cleanup()
|
||||||
|
|
||||||
|
# Register cleanup function
|
||||||
|
import atexit
|
||||||
|
atexit.register(lambda: asyncio.run(cleanup()))
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
"""Parse command line arguments"""
|
||||||
|
parser = argparse.ArgumentParser(description="OpenManus MCP Server")
|
||||||
|
parser.add_argument(
|
||||||
|
"--transport",
|
||||||
|
choices=["stdio", "http"],
|
||||||
|
default="stdio",
|
||||||
|
help="Communication method: stdio or http (default: stdio)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--host",
|
||||||
|
default="127.0.0.1",
|
||||||
|
help="HTTP server host (default: 127.0.0.1)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--port",
|
||||||
|
type=int,
|
||||||
|
default=8000,
|
||||||
|
help="HTTP server port (default: 8000)"
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
if args.transport == "stdio":
|
||||||
|
logger.info("Starting OpenManus server (stdio mode)")
|
||||||
|
openmanus.run(transport="stdio")
|
||||||
|
else:
|
||||||
|
logger.info(f"Starting OpenManus server (HTTP mode) at {args.host}:{args.port}")
|
||||||
|
openmanus.run(transport="http", host=args.host, port=args.port)
|
Loading…
x
Reference in New Issue
Block a user